Compare commits

...

8 Commits

Author SHA1 Message Date
uncle6me-web 0a426c4e16 fix(cypher): CORS origin 空值放行——CLI/curl/MCP 無 Origin 標頭時不再 500 2026-07-31 10:50:57 +08:00
Leo b8ca98cb49 MCP 認證改用 Portal 帳密,廢掉 MCP_OWNER_SECRET(leo 指定做法)
leo:「claude 裡有一個直接輸入帳密連線的,為什麼不用那個?跟他輸入 portal 的帳密一樣不就好了?」

為什麼換(三個封測撞出來的實際問題):
① 沒人給得了封測者——安裝器產生後從不顯示(完成頁 grep「Owner 祕密」=0),
   CF secret 又唯寫讀不回 ⇒ 用戶卡在同意頁,只能找 leo 手動 wrangler 覆寫
② 多一把要記的金鑰——違反「拿一把金鑰就很難了」
③ 全實例共用一把,無法分辨誰連上來(企業多人版必要)

風險評估(leo 判斷,總管原本誇大成「繞過帳密的旁路」已認錯):
secret 要貼進 claude.ai(本身有帳密保護)⇒ 洩漏 secret 與洩漏 portal 帳密風險相同。

改動:
- consent.ts:一個「Owner 祕密」欄位 → email + password 兩欄
- routes.ts POST /authorize:constantTimeEqual(MCP_OWNER_SECRET)
  → 走 CYPHER_EXECUTOR binding 打 /portal/login(認證下沉到唯一真相源,
    同樣吃它的節流與停用檢查)
- routes.ts GET /authorize:移除「未設 MCP_OWNER_SECRET → 503」
  (那是「每個封測者都死在這頁」的直接原因)

驗:tsc 零錯誤;已部署 youlin。
2026-07-31 00:44:15 +08:00
Leo cda1c7b261 instructions 補「你配備了 Arcrun、別上網搜」+新增 skills/INDEX(LLM wiki 式導航)
leo 兩個洞察:
① 「整個 arcrun instruction 很像 LLM wiki…它會拿到一個 index 把所有文件說明都塞給它,
   它不會就可以查,範本全部寫在裡面」
   ⇒ 對照 wiki 的 push/pull 分法:我原本只做了 push(instructions 開場),
     漏了 pull(INDEX 導航)。補 registry/skills/INDEX.md:
     什麼症狀查哪支 skill/查現成零件recipe workflow 的九支工具/四個已知的坑/腹語術紅線
② 「它不去查就不知道…如果它去上網搜因為它不知道自己有配備呢?」
   ⇒ 實測確認 instructions 是 **push**(本 session system prompt 內就有藏書地圖那段)
     ⇒ AI 不會去上網搜,但原本的 instructions 沒說「你配備了什麼」
     ⇒ 開場補:Arcrun 是什麼/你現在就有完整能力/不要上網搜(網路上沒有)

驗:tsc 零錯誤。⚠️ 未部署——實測總管 token 只有 account(read),無法部署任何 CF worker。
2026-07-30 22:38:37 +08:00
Leo fac7d6c9bb AI 一連上 MCP 就知道從哪開始(leo 追問揪出的最後一哩)
leo:「arcrun_get_skill 放在 MCP 裡,現在人類說『幫我用 arcrun 寫 xxx』,
Haiku 會說『arcrun 是什麼?我看看』然後發現有個 arcrun mcp 就去執行?
如果不會,要寫什麼在外面讓它一聽到就知道要用這些資源?」

實測答案:**不會**。
① 總管寫的指引沒有任何入口指向它(真實 AI 找不到)
② MCP 五個 skill 沒有「怎麼寫意圖工作流」這支
③ AI 只看到 41 個工具名 → 自己猜 → 很可能直接 push_workflow 瞎編,
   或去讀 registry/examples 那 8/13 引用不存在零件的壞範例

兩件修法:
1. registry/skills/write_intent_workflow.md — 把指引變成 AI 拿得到的 skill
   (範本全取自實跑 verdict=success 的四支 workflow;含「查詢現在回假 found」的已知限制警告)
2. mcp-handler.ts instructions 開場指路 — instructions 是唯一「AI 一連上就必看」的欄位
   ⇒ 開場就寫明五步順序(先讀 write_intent_workflow → whoami → 查零件 → list_skills → 缺件怎麼補)
   +邊只有兩種、第一個節點是 input、不要因查不到就改寫 code
   ⚠️ 靜態常數:KBDB 掛了也必出現(守「不擋連線」鐵律)

驗:tsc 零錯誤。 待部署後由 leo 真的問 haiku「幫我用 arcrun 做 X」驗證它會不會自己讀 skill。
2026-07-30 22:29:28 +08:00
Leo 0686d39fa1 pending-changes:兩個真缺口提案(引擎條件邊/recipe payload+response+binding)
來源=頂層 CP arcrun-usable 逐步查證。依 leo「照 SDD 做事、照 CP 排順序」,
CP 不得自帶任務 ⇒ 這兩件必須進 SDD 才能做,先走 pending-changes 等 confirm(D35)。

缺口①引擎條件邊:if_control 回 {result,branch} 但 cypher-executor grep ON_TRUE|ON_FALSE=0
⇒ 用了零件仍得寫 code 判斷=「全變成 code」的根。Arcrun#5 於 07-04 發現至今未修。
SDD 查證:7 個字面命中全是「部署分支」等別義,條件邊本身完全沒設計過。

缺口②recipe 缺 payload/response 層:schema 只有 {canonical_id,endpoint,method,auth_service}
⇒ telegram_send 自述「body 帶 chat_id+text」但存不住 body ⇒ 只能繞過 recipe 寫進 workflow。
要補 body_template/response_map/auth:binding 三層。SDD 查證 17 命中全是別的 payload。
2026-07-30 22:06:48 +08:00
Leo 156cdcbe7b chore: 刪掉兩個死零件與其壞範例(leo:這裏的每個不要的零件還存在啊)
📋 SDD:workflow-discovery(active)/對應 CP arcrun-usable 步驟 5「死代碼清除」

刪除(git rm 保留歷史):
- registry/components/km_writer      + .component-builds/km_writer
- registry/components/kbdb_upsert_block + .component-builds/kbdb_upsert_block
- registry/examples/km-wiki-ingest(唯一引用 kbdb_upsert_block 的範例,Mira 時代同源產物)

為什麼刪(Arcrun 自己的 06-mindset.md §1 早已載明):
「mira 的 claude_api / km_writer 就是這樣被錯做成零件的(其實是自用服務膠水)」
+ kbdb_upsert_block 是「一個 CRUD 操作一個零件」(leo:總不能每個都建一個零件)
+ 兩者綁的 Mira 已於 2026-06-29 蒸發=死零件。

刪前實測引用數:workflows.json 0/cypher src 0/examples 僅 km-wiki-ingest。
零件數 22 → 20。

 投影模型驗證成功:build-bundles.mjs 掃 .component-builds/ 目錄(readdirSync),
重跑後 manifest 自動 25 顆 → 23 顆,兩顆消失,**不需手動改 manifest**。
⇒ 證明 manifest 這層本來就是投影(leo 的設計),只有 registry KV 那層是獨立登記簿(待改)。

驗:vitest 9 failed/179 passed=與刪除前相同(既有債)。
2026-07-30 21:30:26 +08:00
Leo 5cadc60e36 feat(workflow-discovery): /cypher/search 改為真查 registry——修「查詢回假信號」
📋 SDD:workflow-discovery(本 commit 同時執行 D35 交接:portal-auth 26/26 完成 → closed
   superseded_by workflow-discovery;workflow-discovery paused → active。單一活性已驗=1 份)
🎯 對應 task:3.x 搜尋端誠實化(CP2-B)

病灶(leo 2026-07-30 定性「腹語術」):
search-nodes.ts 無條件回 status:'found'、missingNodes 永遠 []——
型別宣告了 'missing' 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。
⇒ AI 拿到假信號 → 以為零件存在 → 部署才發現沒有 → 改寫 code
⇒ 正式 workflow 只用 2 個零件、8 個 code 節點含 if×61。

修法:
- 查 registry 判真實存在(走 HTTP,守 D28 禁新增 service binding;
  URL 用既有 wasmWorkerUrl() 慣例組,不自創)
- found 時附 input_schema/success_rate/stability
  ⇒ AI 才填得出 payload、才看得到「測過幾次」(leo:AI 只要填 payload)
- 查不通回 'unknown' 而非 'missing'——**誠實限制**:
  不能因查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code)
- missing 真的回傳出去(原本寫死 [])

⚠️ 實測發現 registry **沒有列表端點**(GET /components → 404,只有 /components/<id>)
⇒ 改為逐個查(節點數通常 <10、5s timeout)。補列表端點後可改抓一次=CP2-B 待辦。

驗:tsc 零錯誤;vitest 9 failed/179 passed=**與改動前 stash 對帳完全相同**(既有債非本次造成)。
 待部署到實例後跑 arcrun-usable/verify.sh 驗 01 那組轉綠。
2026-07-30 20:41:53 +08:00
uncle6me-web 646e689b65 confirm(CP2-F): 二~四刀拆分過裁(leo 授權技術裁決);四題答定(手寫驗證/arcrun-api/排程於 A 線後) 2026-07-24 18:11:10 +08:00
36 changed files with 523 additions and 3934 deletions
@@ -1,14 +0,0 @@
{
"name": "arcrun-kbdb-upsert-block",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"hono": "^4.7.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250408.0",
"typescript": "^5.4.0",
"wrangler": "^4.0.0"
}
}
-898
View File
@@ -1,898 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hono:
specifier: ^4.7.0
version: 4.12.18
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20250408.0
version: 4.20260511.1
typescript:
specifier: ^5.4.0
version: 5.9.3
wrangler:
specifier: ^4.0.0
version: 4.90.1(@cloudflare/workers-types@4.20260511.1)
packages:
'@cloudflare/kv-asset-handler@0.5.0':
resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==}
engines: {node: '>=22.0.0'}
'@cloudflare/unenv-preset@2.16.1':
resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==}
peerDependencies:
unenv: 2.0.0-rc.24
workerd: '>1.20260305.0 <2.0.0-0'
peerDependenciesMeta:
workerd:
optional: true
'@cloudflare/workerd-darwin-64@1.20260508.1':
resolution: {integrity: sha512-IT3r6VgiSwIesL4AJbxjgxvIxwWZqM7BKkhYAzOKHl4GF2M0TxeOahUIXd+CYXVZgHX8ceEg+MXbEehPelJyNg==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20260508.1':
resolution: {integrity: sha512-JTVsisOJPcNKw0qovPjqyBWYahfdhUh7/9NICiG5wxaEQ45PYKdoqNq0hOAAIqvqoxsKZBvTgcPTJREPqk7avA==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20260508.1':
resolution: {integrity: sha512-zO38pCc27YlsZiPYcaZnosy0/t7abXrRU3VEO1oKfUvnaCpHgphDG+VsrmHL+kntda6hrtNwg2jLeMAqqIjnjw==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20260508.1':
resolution: {integrity: sha512-XhJa780Ia6MNIrtxn/ruZHS79b9pu5EKPfRNReaUqxy8erPT2fs93axMfFoS9kIkcaRRj/1TOUKcTeAMoywY7w==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20260508.1':
resolution: {integrity: sha512-QdDOK3B/Ul1s3QmIwDrFyx9230to6LsNmWcVR8w+TYjNZuRPzqQBgusp78LO7MlqCoEl9dvIcN00jkJnLtBSfw==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260511.1':
resolution: {integrity: sha512-FA+si7cOq9i/gtCHhIc0XJL0l1F/ApF+m00752Aj7WZFJrj3ZulT2T8/+rT3BabMT0QEnqFEGIqCgrmqhgEfMg==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@esbuild/aix-ppc64@0.27.3':
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.3':
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.3':
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.3':
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.3':
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.3':
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.3':
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.3':
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.3':
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.3':
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.3':
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.3':
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.3':
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.3':
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.3':
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.3':
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.3':
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.3':
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.3':
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.3':
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.3':
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.3':
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.3':
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.3':
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.3':
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.3':
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@poppinss/colors@4.1.6':
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
'@poppinss/exception@1.2.3':
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@sindresorhus/is@7.2.0':
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
engines: {node: '>=18'}
'@speed-highlight/core@1.2.15':
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hono@4.12.18:
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
engines: {node: '>=16.9.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
miniflare@4.20260508.0:
resolution: {integrity: sha512-h3aG+PA8jEH76V4ZtBAbs3g7kjMfHJUF8hPvxeeajLTKwir+G+dqfBODg5yF9MT29LqrZKCRQRqzfHPWX4kCIg==}
engines: {node: '>=22.0.0'}
hasBin: true
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
semver@7.8.0:
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici@7.24.8:
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
engines: {node: '>=20.18.1'}
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
workerd@1.20260508.1:
resolution: {integrity: sha512-VlnjyH3AjVddpSK7J54nsCVgf8i2733pl8GjKttfNi7vN/hEjjAk20d2b1nDToOLKvRQpTewRnVkqaaeGHCaAw==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.90.1:
resolution: {integrity: sha512-u2KrieKSMfRM0toTst/CfDtcRraeoVjmcExcMWgILM/ytq3qcDhuOAULoZSyPHzma43lfLJy1BC544drFyqe1A==}
engines: {node: '>=22.0.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^4.20260508.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
youch-core@0.3.3:
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
snapshots:
'@cloudflare/kv-asset-handler@0.5.0': {}
'@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1)':
dependencies:
unenv: 2.0.0-rc.24
optionalDependencies:
workerd: 1.20260508.1
'@cloudflare/workerd-darwin-64@1.20260508.1':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20260508.1':
optional: true
'@cloudflare/workerd-linux-64@1.20260508.1':
optional: true
'@cloudflare/workerd-linux-arm64@1.20260508.1':
optional: true
'@cloudflare/workerd-windows-64@1.20260508.1':
optional: true
'@cloudflare/workers-types@4.20260511.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.3':
optional: true
'@esbuild/android-arm64@0.27.3':
optional: true
'@esbuild/android-arm@0.27.3':
optional: true
'@esbuild/android-x64@0.27.3':
optional: true
'@esbuild/darwin-arm64@0.27.3':
optional: true
'@esbuild/darwin-x64@0.27.3':
optional: true
'@esbuild/freebsd-arm64@0.27.3':
optional: true
'@esbuild/freebsd-x64@0.27.3':
optional: true
'@esbuild/linux-arm64@0.27.3':
optional: true
'@esbuild/linux-arm@0.27.3':
optional: true
'@esbuild/linux-ia32@0.27.3':
optional: true
'@esbuild/linux-loong64@0.27.3':
optional: true
'@esbuild/linux-mips64el@0.27.3':
optional: true
'@esbuild/linux-ppc64@0.27.3':
optional: true
'@esbuild/linux-riscv64@0.27.3':
optional: true
'@esbuild/linux-s390x@0.27.3':
optional: true
'@esbuild/linux-x64@0.27.3':
optional: true
'@esbuild/netbsd-arm64@0.27.3':
optional: true
'@esbuild/netbsd-x64@0.27.3':
optional: true
'@esbuild/openbsd-arm64@0.27.3':
optional: true
'@esbuild/openbsd-x64@0.27.3':
optional: true
'@esbuild/openharmony-arm64@0.27.3':
optional: true
'@esbuild/sunos-x64@0.27.3':
optional: true
'@esbuild/win32-arm64@0.27.3':
optional: true
'@esbuild/win32-ia32@0.27.3':
optional: true
'@esbuild/win32-x64@0.27.3':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@poppinss/colors@4.1.6':
dependencies:
kleur: 4.1.5
'@poppinss/dumper@0.6.5':
dependencies:
'@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.2.0
supports-color: 10.2.2
'@poppinss/exception@1.2.3': {}
'@sindresorhus/is@7.2.0': {}
'@speed-highlight/core@1.2.15': {}
blake3-wasm@2.1.5: {}
cookie@1.1.1: {}
detect-libc@2.1.2: {}
error-stack-parser-es@1.0.5: {}
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
'@esbuild/android-arm': 0.27.3
'@esbuild/android-arm64': 0.27.3
'@esbuild/android-x64': 0.27.3
'@esbuild/darwin-arm64': 0.27.3
'@esbuild/darwin-x64': 0.27.3
'@esbuild/freebsd-arm64': 0.27.3
'@esbuild/freebsd-x64': 0.27.3
'@esbuild/linux-arm': 0.27.3
'@esbuild/linux-arm64': 0.27.3
'@esbuild/linux-ia32': 0.27.3
'@esbuild/linux-loong64': 0.27.3
'@esbuild/linux-mips64el': 0.27.3
'@esbuild/linux-ppc64': 0.27.3
'@esbuild/linux-riscv64': 0.27.3
'@esbuild/linux-s390x': 0.27.3
'@esbuild/linux-x64': 0.27.3
'@esbuild/netbsd-arm64': 0.27.3
'@esbuild/netbsd-x64': 0.27.3
'@esbuild/openbsd-arm64': 0.27.3
'@esbuild/openbsd-x64': 0.27.3
'@esbuild/openharmony-arm64': 0.27.3
'@esbuild/sunos-x64': 0.27.3
'@esbuild/win32-arm64': 0.27.3
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
fsevents@2.3.3:
optional: true
hono@4.12.18: {}
kleur@4.1.5: {}
miniflare@4.20260508.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
sharp: 0.34.5
undici: 7.24.8
workerd: 1.20260508.1
ws: 8.18.0
youch: 4.1.0-beta.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
semver@7.8.0: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.0
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
supports-color@10.2.2: {}
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
undici@7.24.8: {}
unenv@2.0.0-rc.24:
dependencies:
pathe: 2.0.3
workerd@1.20260508.1:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20260508.1
'@cloudflare/workerd-darwin-arm64': 1.20260508.1
'@cloudflare/workerd-linux-64': 1.20260508.1
'@cloudflare/workerd-linux-arm64': 1.20260508.1
'@cloudflare/workerd-windows-64': 1.20260508.1
wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.5.0
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1)
blake3-wasm: 2.1.5
esbuild: 0.27.3
miniflare: 4.20260508.0
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.24
workerd: 1.20260508.1
optionalDependencies:
'@cloudflare/workers-types': 4.20260511.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.18.0: {}
youch-core@0.3.3:
dependencies:
'@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5
youch@4.1.0-beta.10:
dependencies:
'@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.15
cookie: 1.1.1
youch-core: 0.3.3
@@ -1,8 +0,0 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
@@ -1,81 +0,0 @@
/**
* arcrun WASM 零件 Worker (kbdb_upsert_block)
* POST / → JSON input → WASM (WASI preview1) → JSON output
* SDD: polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1
* matrix/arcrun/.agents/specs/arcrun/arcrun.md 三-B 新零件加入紀錄
*/
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim';
const app = new Hono();
app.use('*', cors());
app.get('/', (c) => c.json({ ok: true, component: 'kbdb_upsert_block' }));
app.post('/', async (c) => {
let input: unknown;
try {
input = await c.req.json();
} catch {
return c.json({ success: false, error: 'request body must be JSON' }, 400);
}
try {
const result = await runWasm(input);
return c.json(result);
} catch (e) {
return c.json(
{ success: false, error: e instanceof Error ? e.message : String(e) },
500,
);
}
});
export default app;
async function runWasm(input: unknown): Promise<unknown> {
const hostFunctions: WasiHostFunctions = {
http_request: async (url, method, headersJson, body) => {
const headers: Record<string, string> = {};
if (headersJson) {
try {
const parsed = JSON.parse(headersJson);
if (parsed && typeof parsed === 'object') {
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
if (typeof v === 'string') headers[k] = v;
}
}
} catch {}
}
const init: RequestInit = { method, headers };
if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
init.body = body;
}
const res = await fetch(url, init);
const text = await res.text();
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
if (!res.ok) {
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
}
return text;
},
};
const shim = createWasiShim(JSON.stringify(input), hostFunctions);
const instance = await WebAssembly.instantiate(
componentWasm as WebAssembly.Module,
shim.imports,
);
shim.setMemory(instance.exports.memory as WebAssembly.Memory);
await shim.run(instance);
const stdout = shim.getStdout().trim();
const stderr = shim.getStderr().trim();
if (stderr) console.error('[kbdb_upsert_block wasm stderr]', stderr);
if (!stdout) throw new Error('WASM component produced no output');
return JSON.parse(stdout);
}
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true
}
}
@@ -1,12 +0,0 @@
name = "arcrun-kbdb-upsert-block"
main = "src/index.ts"
compatibility_date = "2025-02-19"
compatibility_flags = ["nodejs_compat"]
workers_dev = true
[vars]
COMPONENT_ID = "kbdb_upsert_block"
[[routes]]
pattern = "kbdb-upsert-block.arcrun.dev/*"
zone_name = "arcrun.dev"
-14
View File
@@ -1,14 +0,0 @@
{
"name": "arcrun-km-writer",
"version": "1.0.0",
"private": true,
"type": "module",
"dependencies": {
"hono": "^4.7.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250408.0",
"typescript": "^5.4.0",
"wrangler": "^4.0.0"
}
}
-898
View File
@@ -1,898 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hono:
specifier: ^4.7.0
version: 4.12.14
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20250408.0
version: 4.20260420.1
typescript:
specifier: ^5.4.0
version: 5.9.3
wrangler:
specifier: ^4.0.0
version: 4.83.0(@cloudflare/workers-types@4.20260420.1)
packages:
'@cloudflare/kv-asset-handler@0.4.2':
resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==}
engines: {node: '>=18.0.0'}
'@cloudflare/unenv-preset@2.16.0':
resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==}
peerDependencies:
unenv: 2.0.0-rc.24
workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0
peerDependenciesMeta:
workerd:
optional: true
'@cloudflare/workerd-darwin-64@1.20260415.1':
resolution: {integrity: sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20260415.1':
resolution: {integrity: sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20260415.1':
resolution: {integrity: sha512-tU+9pwsqCy8afOVlGtiWrWQc/fedQK4SRm4KPIAt+zOiQWDxWASm6YGBUJis5c648WN80yz47qnmdDi8DQNOcA==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20260415.1':
resolution: {integrity: sha512-bR9uITnV19r5NQ14xnypi2xHXu2iQvfYV8cVgx0JouFUmWwTEEAwFVojDdssGq93VHX9hr/pi2IRUZeegbYBog==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20260415.1':
resolution: {integrity: sha512-4NuMLlerI0Ijua3Ir8HXQ+qyNvCUDEG5gDco5Om+sAiK6rnWiz+aGoSlbB8W16yW9QAgzCstbmXLiVknUBflfQ==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260420.1':
resolution: {integrity: sha512-DHT9JnSn9cIiCSdL76OxW+Xvc1+ml1CWzWvgVwreoHQ+E604aeFxPPHp9X7nE+XRWm2NH4l0OgtxUI5T/nuI3g==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@esbuild/aix-ppc64@0.27.3':
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.3':
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.3':
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.3':
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.3':
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.3':
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.3':
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.3':
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.3':
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.3':
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.3':
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.3':
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.3':
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.3':
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.3':
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.3':
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.3':
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.3':
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.3':
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.3':
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.3':
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.3':
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.3':
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.3':
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.3':
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.3':
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.34.5':
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@poppinss/colors@4.1.6':
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
'@poppinss/exception@1.2.3':
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@sindresorhus/is@7.2.0':
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
engines: {node: '>=18'}
'@speed-highlight/core@1.2.15':
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
cookie@1.1.1:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
error-stack-parser-es@1.0.5:
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
esbuild@0.27.3:
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
engines: {node: '>=18'}
hasBin: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
hono@4.12.14:
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
engines: {node: '>=16.9.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
miniflare@4.20260415.0:
resolution: {integrity: sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng==}
engines: {node: '>=18.0.0'}
hasBin: true
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
supports-color@10.2.2:
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
engines: {node: '>=18'}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici@7.24.8:
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
engines: {node: '>=20.18.1'}
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
workerd@1.20260415.1:
resolution: {integrity: sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.83.0:
resolution: {integrity: sha512-gw5g3LCiuAqVWxaoKY6+quE0HzAUEFb/FV3oAlNkE1ttd4XP3FiV91XDkkzUCcdqxS4WjhQvPhIDBNdhEi8P0A==}
engines: {node: '>=20.3.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^4.20260415.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
youch-core@0.3.3:
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
youch@4.1.0-beta.10:
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
snapshots:
'@cloudflare/kv-asset-handler@0.4.2': {}
'@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1)':
dependencies:
unenv: 2.0.0-rc.24
optionalDependencies:
workerd: 1.20260415.1
'@cloudflare/workerd-darwin-64@1.20260415.1':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20260415.1':
optional: true
'@cloudflare/workerd-linux-64@1.20260415.1':
optional: true
'@cloudflare/workerd-linux-arm64@1.20260415.1':
optional: true
'@cloudflare/workerd-windows-64@1.20260415.1':
optional: true
'@cloudflare/workers-types@4.20260420.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.27.3':
optional: true
'@esbuild/android-arm64@0.27.3':
optional: true
'@esbuild/android-arm@0.27.3':
optional: true
'@esbuild/android-x64@0.27.3':
optional: true
'@esbuild/darwin-arm64@0.27.3':
optional: true
'@esbuild/darwin-x64@0.27.3':
optional: true
'@esbuild/freebsd-arm64@0.27.3':
optional: true
'@esbuild/freebsd-x64@0.27.3':
optional: true
'@esbuild/linux-arm64@0.27.3':
optional: true
'@esbuild/linux-arm@0.27.3':
optional: true
'@esbuild/linux-ia32@0.27.3':
optional: true
'@esbuild/linux-loong64@0.27.3':
optional: true
'@esbuild/linux-mips64el@0.27.3':
optional: true
'@esbuild/linux-ppc64@0.27.3':
optional: true
'@esbuild/linux-riscv64@0.27.3':
optional: true
'@esbuild/linux-s390x@0.27.3':
optional: true
'@esbuild/linux-x64@0.27.3':
optional: true
'@esbuild/netbsd-arm64@0.27.3':
optional: true
'@esbuild/netbsd-x64@0.27.3':
optional: true
'@esbuild/openbsd-arm64@0.27.3':
optional: true
'@esbuild/openbsd-x64@0.27.3':
optional: true
'@esbuild/openharmony-arm64@0.27.3':
optional: true
'@esbuild/sunos-x64@0.27.3':
optional: true
'@esbuild/win32-arm64@0.27.3':
optional: true
'@esbuild/win32-ia32@0.27.3':
optional: true
'@esbuild/win32-x64@0.27.3':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-ppc64@1.2.4':
optional: true
'@img/sharp-libvips-linux-riscv64@1.2.4':
optional: true
'@img/sharp-libvips-linux-s390x@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-ppc64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.2.4
optional: true
'@img/sharp-linux-riscv64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.2.4
optional: true
'@img/sharp-linux-s390x@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.10.0
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-ia32@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@poppinss/colors@4.1.6':
dependencies:
kleur: 4.1.5
'@poppinss/dumper@0.6.5':
dependencies:
'@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.2.0
supports-color: 10.2.2
'@poppinss/exception@1.2.3': {}
'@sindresorhus/is@7.2.0': {}
'@speed-highlight/core@1.2.15': {}
blake3-wasm@2.1.5: {}
cookie@1.1.1: {}
detect-libc@2.1.2: {}
error-stack-parser-es@1.0.5: {}
esbuild@0.27.3:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.3
'@esbuild/android-arm': 0.27.3
'@esbuild/android-arm64': 0.27.3
'@esbuild/android-x64': 0.27.3
'@esbuild/darwin-arm64': 0.27.3
'@esbuild/darwin-x64': 0.27.3
'@esbuild/freebsd-arm64': 0.27.3
'@esbuild/freebsd-x64': 0.27.3
'@esbuild/linux-arm': 0.27.3
'@esbuild/linux-arm64': 0.27.3
'@esbuild/linux-ia32': 0.27.3
'@esbuild/linux-loong64': 0.27.3
'@esbuild/linux-mips64el': 0.27.3
'@esbuild/linux-ppc64': 0.27.3
'@esbuild/linux-riscv64': 0.27.3
'@esbuild/linux-s390x': 0.27.3
'@esbuild/linux-x64': 0.27.3
'@esbuild/netbsd-arm64': 0.27.3
'@esbuild/netbsd-x64': 0.27.3
'@esbuild/openbsd-arm64': 0.27.3
'@esbuild/openbsd-x64': 0.27.3
'@esbuild/openharmony-arm64': 0.27.3
'@esbuild/sunos-x64': 0.27.3
'@esbuild/win32-arm64': 0.27.3
'@esbuild/win32-ia32': 0.27.3
'@esbuild/win32-x64': 0.27.3
fsevents@2.3.3:
optional: true
hono@4.12.14: {}
kleur@4.1.5: {}
miniflare@4.20260415.0:
dependencies:
'@cspotcode/source-map-support': 0.8.1
sharp: 0.34.5
undici: 7.24.8
workerd: 1.20260415.1
ws: 8.18.0
youch: 4.1.0-beta.10
transitivePeerDependencies:
- bufferutil
- utf-8-validate
path-to-regexp@6.3.0: {}
pathe@2.0.3: {}
semver@7.7.4: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.7.4
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-libvips-darwin-arm64': 1.2.4
'@img/sharp-libvips-darwin-x64': 1.2.4
'@img/sharp-libvips-linux-arm': 1.2.4
'@img/sharp-libvips-linux-arm64': 1.2.4
'@img/sharp-libvips-linux-ppc64': 1.2.4
'@img/sharp-libvips-linux-riscv64': 1.2.4
'@img/sharp-libvips-linux-s390x': 1.2.4
'@img/sharp-libvips-linux-x64': 1.2.4
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-ppc64': 0.34.5
'@img/sharp-linux-riscv64': 0.34.5
'@img/sharp-linux-s390x': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-wasm32': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-ia32': 0.34.5
'@img/sharp-win32-x64': 0.34.5
supports-color@10.2.2: {}
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
undici@7.24.8: {}
unenv@2.0.0-rc.24:
dependencies:
pathe: 2.0.3
workerd@1.20260415.1:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20260415.1
'@cloudflare/workerd-darwin-arm64': 1.20260415.1
'@cloudflare/workerd-linux-64': 1.20260415.1
'@cloudflare/workerd-linux-arm64': 1.20260415.1
'@cloudflare/workerd-windows-64': 1.20260415.1
wrangler@4.83.0(@cloudflare/workers-types@4.20260420.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.2
'@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1)
blake3-wasm: 2.1.5
esbuild: 0.27.3
miniflare: 4.20260415.0
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.24
workerd: 1.20260415.1
optionalDependencies:
'@cloudflare/workers-types': 4.20260420.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.18.0: {}
youch-core@0.3.3:
dependencies:
'@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5
youch@4.1.0-beta.10:
dependencies:
'@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.15
cookie: 1.1.1
youch-core: 0.3.3
@@ -1,8 +0,0 @@
allowBuilds:
esbuild: true
sharp: true
workerd: true
onlyBuiltDependencies:
- esbuild
- sharp
- workerd
-83
View File
@@ -1,83 +0,0 @@
/**
* arcrun API component Worker (km_writer)
*
* POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output
*
* 提供 http_request host function,讓 WASM 零件呼叫 Mira /km/* API。
*/
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim';
const app = new Hono();
app.use('*', cors());
app.get('/', (c) => c.json({ ok: true, component: 'km_writer' }));
app.post('/', async (c) => {
let input: unknown;
try {
input = await c.req.json();
} catch {
return c.json({ success: false, error: 'request body must be JSON' }, 400);
}
try {
const result = await runWasm(input);
return c.json(result);
} catch (e) {
return c.json(
{ success: false, error: e instanceof Error ? e.message : String(e) },
500,
);
}
});
export default app;
async function runWasm(input: unknown): Promise<unknown> {
const hostFunctions: WasiHostFunctions = {
http_request: async (url, method, headersJson, body) => {
const headers: Record<string, string> = {};
if (headersJson) {
try {
const parsed = JSON.parse(headersJson);
if (parsed && typeof parsed === 'object') {
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
if (typeof v === 'string') headers[k] = v;
}
}
} catch {
// ignore header parse errors
}
}
const init: RequestInit = { method, headers };
if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
init.body = body;
}
const res = await fetch(url, init);
const text = await res.text();
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
if (!res.ok) {
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
}
return text;
},
};
const shim = createWasiShim(JSON.stringify(input), hostFunctions);
const instance = await WebAssembly.instantiate(
componentWasm as WebAssembly.Module,
shim.imports,
);
shim.setMemory(instance.exports.memory as WebAssembly.Memory);
await shim.run(instance);
const stdout = shim.getStdout().trim();
if (!stdout) throw new Error('WASM component produced no output');
return JSON.parse(stdout);
}
-11
View File
@@ -1,11 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true
}
}
-12
View File
@@ -1,12 +0,0 @@
name = "arcrun-km-writer"
main = "src/index.ts"
compatibility_date = "2025-02-19"
compatibility_flags = ["nodejs_compat"]
workers_dev = true
[vars]
COMPONENT_ID = "km_writer"
[[routes]]
pattern = "km-writer.arcrun.dev/*"
zone_name = "arcrun.dev"
@@ -17,10 +17,12 @@ export async function handleCypherSearch(
throw new Error('無法解析任何節點');
}
const { nodeResults } = searchNodes(parsed);
// 2026-07-30:查 registry 判真實存在(workflow-discovery)。
// `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env);
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] };
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes };
}
export async function handleCypherExecute(
@@ -50,7 +52,7 @@ export async function handleCypherExecute(
throw new Error('無法解析任何節點');
}
const { nodeResults } = searchNodes(parsed, config);
const { nodeResults } = await searchNodes(parsed, config, env);
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
const parseResult = graphSchema.safeParse(graph);
+116 -13
View File
@@ -1,27 +1,53 @@
import type { ParsedTriplets, NodeRole } from './triplet-parser';
import { resolveNodeRole } from './triplet-parser';
import { wasmWorkerUrl } from '../lib/component-loader';
export type NodeStatus = 'found' | 'missing' | 'unknown';
export type NodeInfo = {
status: NodeStatus;
componentId?: string;
type: NodeRole;
/** 零件契約(found 時附上,讓 AI 知道怎麼填 payload)。 */
input_schema?: unknown;
/** 成功率(found 時附上,讓「被測過幾次」看得見)。 */
success_rate?: number;
stability?: string;
/** missing 時給的相近零件建議(避免 AI 只知道「沒有」卻不知道該用什麼)。 */
suggestions?: string[];
};
export type SearchResult = {
nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }>;
nodeResults: Record<string, NodeInfo>;
missingNodes: string[];
};
/**
* 對所有節點進行解析,確認每個節點對應的零件 ID。
* 對所有節點進行解析,確認每個節點對應的零件 ID 與**是否真的存在**
*
* 注意:此步驟只做靜態解析,不做遠端查找
* 零件是否真的存在由 component-loader 在執行時決定(Service Binding / KV / URL)。
* ⚠️ 2026-07-30 改為會查 registryworkflow-discovery task 3.x
*
* 優先序:
* 1. Input/Output 角色:自動標記,componentId = 小寫節點名稱
* 2. config[nodeName].component 已指定:使用 config 提供的 componentId
* 3. 其他:componentId = 節點名稱(交給 component-loader 在執行時解析)
* 改之前的行為(病灶):無條件回 `status: 'found'`、`missingNodes` 永遠是空陣列——
* 型別雖宣告了 `'missing'` 但程式碼從不使用。實測「完全不存在的東西xyz」也回 found。
*
* 為什麼這是嚴重問題(leo 2026-07-30 定性「腹語術」):
* AI 寫意圖 → 查詢回「都 found」(假信號)→ 實際零件不存在
* → 部署/執行才發現 → 最快的修法是改寫成 `code` 節點自己寫 JS
* → 於是正式 workflow 只用 2 個零件、8 個 code 節點含 if×61
* ⇒ 「零件被測過 1000 次所以 AI 只要填 payload」的價值完全落空。
*
* 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'missing'`——
* 不能因為查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code,正是要避免的事)。
*/
export function searchNodes(
export async function searchNodes(
parsed: ParsedTriplets,
config?: Record<string, Record<string, unknown>>,
): SearchResult {
const nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }> = {};
env?: { WORKER_SUBDOMAIN?: string },
): Promise<SearchResult> {
const nodeResults: Record<string, NodeInfo> = {};
const missingNodes: string[] = [];
const sub = env?.WORKER_SUBDOMAIN;
for (const nodeName of parsed.nodeNames) {
const role = resolveNodeRole(nodeName, parsed);
@@ -33,8 +59,85 @@ export function searchNodes(
const configComponent = config?.[nodeName]?.component as string | undefined;
const componentId = configComponent ?? nodeName;
nodeResults[nodeName] = { status: 'found', componentId, type: role };
// config 明確給了 component(多半是安裝器代入的 worker URL 或既有 workflow
// → 不判 missing。這條路徑的存在性由 component-loader 在執行時決定(原行為)。
if (configComponent) {
nodeResults[nodeName] = { status: 'found', componentId, type: role };
continue;
}
if (!sub) {
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
continue;
}
const q = await fetchComponent(sub, componentId);
if (!q.ok) {
// registry 查不通(未部署/網路失敗)⇒ 誠實回 unknown。
// **不能誤判 missing**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
continue;
}
if (q.entry) {
nodeResults[nodeName] = {
status: 'found',
componentId,
type: role,
input_schema: q.entry.input_schema,
success_rate: q.entry.success_rate,
stability: q.entry.stability,
};
continue;
}
nodeResults[nodeName] = { status: 'missing', componentId, type: role };
missingNodes.push(nodeName);
}
return { nodeResults, missingNodes: [] };
return { nodeResults, missingNodes };
}
type CatalogEntry = {
input_schema?: unknown;
success_rate?: number;
stability?: string;
};
/**
* 查單一零件是否存在於 registry。
*
* ⚠️ 為什麼逐個查而非抓整份目錄:registry **沒有列表端點**
* (實測 `GET /components` → 404,只有 `GET /components/<id>`)。
* 這是 CP2-B 記載的缺口(「修 /components 404」)——補了列表端點後可改為抓一次。
* 現階段逐個查:節點數通常 <10,且有 5s timeout,可接受。
*
* 回傳 `null` 代表「查不到 registry 或該零件不存在」,由呼叫端區分:
* 整體查不通 → `unknown`;查得通但這顆沒有 → `missing`。
*/
async function fetchComponent(
subdomain: string,
id: string,
): Promise<{ ok: boolean; entry?: CatalogEntry }> {
try {
const base = wasmWorkerUrl('registry', subdomain);
const res = await fetch(`${base}/components/${encodeURIComponent(id)}`, {
signal: AbortSignal.timeout(5000),
});
if (res.status === 404) return { ok: true }; // registry 活著,但沒這顆
if (!res.ok) return { ok: false };
const body = (await res.json()) as { success?: boolean; data?: Record<string, unknown> };
if (body.success === false) return { ok: true }; // 同上:回「零件不存在」
const d = body.data ?? (body as unknown as Record<string, unknown>);
return {
ok: true,
entry: {
input_schema: d.input_schema,
success_rate: typeof d.success_rate === 'number' ? d.success_rate : undefined,
stability: typeof d.stability === 'string' ? d.stability : undefined,
},
};
} catch {
return { ok: false };
}
}
@@ -14,7 +14,7 @@ export async function resolveWebhookGraph(
const parsed = parseTriplets(body.triplets as unknown[]);
if (!parsed) return { resolvedGraph: {}, error: '無法解析 triplets' };
const { nodeResults } = searchNodes(parsed);
const { nodeResults } = await searchNodes(parsed);
const graphId = `webhook-${Date.now()}`;
const graphName = description || `Webhook ${new Date().toISOString()}`;
+9 -4
View File
@@ -39,10 +39,15 @@ const STATIC_ORIGINS = ['https://arcrun.dev', 'https://www.arcrun.dev'];
app.use('*', cors({
origin: (origin, c) => {
const extra = (c.env.UI_ORIGINS || '')
.split(',')
.map((s: string) => s.trim())
.filter(Boolean);
// ⚠️ 非瀏覽器請求(CLIcurlMCP)沒有 Origin 標頭 → origin 是空字串/undefined。
// 此時必須原樣放行,不能回 null——回 null 會讓 Hono cors 中介層在後續處理拋錯,
// 表現為所有 CLI 部署一律 5002026-07-21 實撞:acr push 全掛,對照組亦然)。
if (!origin) return origin;
let extra: string[] = [];
try {
extra = String((c.env as Record<string, unknown>).UI_ORIGINS || '')
.split(',').map((s: string) => s.trim()).filter(Boolean);
} catch { /* UI_ORIGINS 未設定=只用靜態白名單 */ }
return [...STATIC_ORIGINS, ...extra].includes(origin) ? origin : null;
},
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
+33 -1
View File
@@ -15,10 +15,42 @@ export async function handleMcpRequest(
//(選型理由見 lib/library-map.ts 檔頭);任何失敗回 null → 靜默略過,絕不擋 MCP 連線(鐵律)。
const mapInstructions = await buildLibraryMapInstructions(env);
// 2026-07-30leo 問「人類說『幫我用 arcrun 寫 xxx』,Haiku 會知道要用這些資源嗎?
// 如果不會,要寫什麼在外面讓它一聽到就知道?」):
// 實測答案是**不會**——AI 只看到 41 個工具名,會自己猜(很可能直接跳到 push_workflow
// 瞎編,或去讀 registry/examples 那 8/13 引用不存在零件的壞範例)。
// instructions 是唯一「AI 一連上就必看」的欄位 ⇒ 開場就指路,不依賴任何查詢成功。
// ⚠️ 這段是靜態常數:即使 KBDB 掛了、藏書地圖抓不到,它也必須出現(鐵律:不擋連線)。
const startHere = [
"# Arcrun — 你已經配備了這套工具,別上網找",
"",
"**Arcrun 是什麼**:跑在 Cloudflare 上的工作流引擎(類 n8n)。你用 `>>` 寫「意圖」,",
"系統告訴你有哪些現成零件/recipe 可用,你只要填 payload——**不必自己寫程式**。",
"**你現在就有完整能力**:查零件、查 recipe、看實跑過的 workflow、部署、觸發、看執行紀錄。",
"⚠️ **不要上網搜 Arcrun 文件**(網路上沒有/會過時)。答案都在下面的工具裡。",
"",
"【先讀這裡】要在 Arcrun 上做任何事(用戶說「幫我用 Arcrun 做 X」),**照這個順序**",
"",
"1. `arcrun_get_skill('write_intent_workflow')` — **必讀第一支**。",
" 教你用 `>>` 寫「意圖工作流」。你**不需要先知道有哪些零件**,先寫意圖。",
"2. `arcrun_whoami()` — 確認連到哪個帳號(勿自行 curl 猜帳號 URL)。",
"3. 把意圖丟 `POST /cypher/search` 或 `arcrun_validate_yaml` — 系統告訴你哪些零件存在。",
"4. 卡住/不知道該查什麼 → `arcrun_get_skill('INDEX')`(全館導航:什麼問題查哪裡+已知的坑)。",
"5. 缺零件時:缺 API → 寫 recipe`arcrun_recipe_push`);缺能力 → 投稿零件 PR。",
" 🔴 **不要因為查不到零件就改寫成 `code` 節點**——那叫「腹語術」(表面用 Arcrun、",
" 實際全寫 JS)。`code` 只用於局部整形(例:剝掉 LLM 回應的雜訊)。",
"",
"邊只有兩種:`ON_SUCCESS` 與 `對每個 <變數>`FOREACH)。",
"**沒有** `ON_TRUE``ON_FALSE``ON_FAILURE`——引擎目前不支援條件分支。",
"第一個節點固定是 `input`。",
].join("\n");
const instructions = mapInstructions ? `${startHere}\n\n---\n\n${mapInstructions}` : startHere;
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
const server = new McpServer(
{ name: "arcrun-mcp-server", version: "1.0.0" },
mapInstructions ? { instructions: mapInstructions } : undefined,
{ instructions },
);
registerAllTools(server, env, orgNamespace, partnerToken);
+9 -5
View File
@@ -59,7 +59,7 @@ export function consentPage(p: ConsentParams, error?: string): string {
code { background: rgba(127,127,127,.15); padding: .1rem .35rem; border-radius: .25rem;
font-size: .85rem; word-break: break-all; }
label { display: block; margin: 1.25rem 0 .35rem; font-weight: 600; }
input[type=password] { width: 100%; padding: .6rem .7rem; font-size: 1rem;
input[type=password], input[type=email] { width: 100%; padding: .6rem .7rem; font-size: 1rem;
border: 1px solid #8888; border-radius: .5rem; box-sizing: border-box; }
button { margin-top: 1.25rem; width: 100%; padding: .7rem; font-size: 1rem; font-weight: 600;
border: 0; border-radius: .5rem; background: #8a5f1e; color: #fff; cursor: pointer; }
@@ -75,12 +75,16 @@ export function consentPage(p: ConsentParams, error?: string): string {
${errBlock}
<form method="POST" action="/authorize">
${fields}
<label for="owner_secret">Owner 祕密</label>
<input id="owner_secret" name="owner_secret" type="password" autocomplete="off"
autofocus required placeholder="只有你知道的祕密">
<label for="email">你的 Portal 帳號(email</label>
<input id="email" name="email" type="email" autocomplete="username"
autofocus required placeholder="你登入知識庫用的 email">
<label for="password">Portal 密碼</label>
<input id="password" name="password" type="password" autocomplete="current-password"
required placeholder="你登入知識庫用的密碼">
<button type="submit">授權連線</button>
</form>
<p class="foot">祕密不正確不會發出授權碼。此頁不儲存你的輸入。</p>
<p class="foot">用你登入知識庫(Portal)的同一組帳密,**不需要另外的祕密**。
帳密不正確不會發出授權碼。此頁不儲存你的輸入。</p>
</body>
</html>`;
}
+35 -10
View File
@@ -184,9 +184,8 @@ export function registerOAuthRoutes<
302,
);
}
if (!c.env.MCP_OWNER_SECRET) {
return c.text("server_error: MCP_OWNER_SECRET not configured", 503);
}
// 2026-07-30:不再檢查 MCP_OWNER_SECRET(改用 Portal 帳密驗證,見 POST 分支)。
// 舊行為:未設此 env → 直接 503 ⇒ **每個封測者接自己的 AI 都死在這頁**。
const params: ConsentParams = {
client_id: q.client_id ?? "",
redirect_uri: q.redirect_uri,
@@ -222,9 +221,6 @@ export function registerOAuthRoutes<
302,
);
}
if (!c.env.MCP_OWNER_SECRET) {
return c.text("server_error: MCP_OWNER_SECRET not configured", 503);
}
const consent: ConsentParams = {
client_id: p.client_id ?? "",
redirect_uri: redirectUri,
@@ -234,10 +230,39 @@ export function registerOAuthRoutes<
scope: p.scope ?? "mcp",
resource: canonicalResource, // 一律存 canonical,不存 client 原樣值
};
// ★ owner 祕密把關:錯誤不發碼、重顯同意頁。這是「只知 URL 的人進不來」的唯一閘。
const supplied = p.owner_secret ?? "";
if (!supplied || !constantTimeEqual(supplied, c.env.MCP_OWNER_SECRET)) {
return c.html(consentPage(consent, "Owner 祕密不正確,請重試。"), 401);
// ★ 把關:用**用戶自己的 Portal 帳密**,不再另設一把 MCP_OWNER_SECRET
// leo 2026-07-30:「claude 裡有一個直接輸入帳密連線的,為什麼不用那個?
// 跟他輸入 portal 的帳密一樣不就好了?」)
//
// 為什麼換掉 owner secret(三個實際問題,都是封測撞出來的):
// ① **沒人給得了封測者**——安裝器產生後從不顯示(完成頁 grep「Owner 祕密」=0),
// CF secret 又唯寫讀不回 ⇒ 用戶卡在這頁,只能找 leo 手動 wrangler 覆寫
// ② **多一把要記的金鑰**——違反「拿一把金鑰就很難了」(D36 精神)
// ③ **全實例共用一把**,無法分辨是誰連上來的(企業多人版必要)
// 風險評估(leo 判斷,總管原本誇大成「繞過帳密的旁路」已更正):
// secret 要貼進 claude.ai(本身有帳密保護)⇒ 洩漏 secret 與洩漏 portal 帳密風險相同。
const email = (p.email ?? "").trim();
const password = p.password ?? "";
if (!email || !password) {
return c.html(consentPage(consent, "請輸入你的 Portal 帳號與密碼。"), 401);
}
// 認證下沉到 cypher 的 /portal/login(唯一真相源;同樣吃它的節流與停用檢查)。
// 走 service bindingMCP 與 cypher 同帳號,屬 D28 允許的零件級組合)。
let loginOk = false;
try {
const res = await c.env.CYPHER_EXECUTOR.fetch(
new Request("https://cypher/portal/login", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email, password }),
}),
);
loginOk = res.ok;
} catch {
return c.html(consentPage(consent, "暫時無法驗證帳密,請稍後再試。"), 503);
}
if (!loginOk) {
return c.html(consentPage(consent, "帳號或密碼不正確,請重試。"), 401);
}
if (!c.env.OAUTH_KV) {
return c.text("server_error: OAUTH_KV not configured", 503);
@@ -1,90 +0,0 @@
canonical_id: "kbdb_upsert_block"
display_name: "KBDB Upsert Block"
category: "data"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
- "wazero"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: false
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [api_key, page_name, content]
properties:
api_key:
type: string
description: KBDB partner keyak_xxx
page_name:
type: string
description: 當 idempotency key。內部用 GET /blocks?page_name= 查找。
content:
type: string
description: block 內容(PATCH 時覆寫,CREATE 時新建)
type:
type: string
description: block type(建立時用,PATCH 時忽略)
parent_id:
type: string
description: 父 block id(建立時用,PATCH 時忽略)
user_id:
type: string
description: 建立時帶入 + lookup 時用來 filter(同 page_name 多 user 共存場景)
source:
type: string
description: 來源標記
tags_json:
type: string
description: tags JSON 字串(PATCH 時轉 array、CREATE 時直傳)
kbdb_url:
type: string
description: KBDB API base(預設 https://kbdb.finally.click
output_schema:
type: object
properties:
success:
type: boolean
action:
type: string
enum: [created, patched]
description: 實際做了哪個動作
data:
type: object
description: KBDB 回傳(含 block id 等)
error:
type: string
phase:
type: string
enum: [lookup, patch, create]
description: 出錯在哪個階段
gherkin_tests:
- scenario: "缺 page_name"
given: '{"api_key":"ak_x","content":"hi"}'
then_contains: '"success":false'
- scenario: "建立新 block"
given: '{"api_key":"ak_x","page_name":"new-page-uniq","content":"hello"}'
then_contains: '"action":"created"'
- scenario: "PATCH 既有 block"
given: '{"api_key":"ak_x","page_name":"existing-page","content":"updated"}'
then_contains: '"action":"patched"'
tags: [data, storage, kbdb, upsert, primitive, idempotent]
description: |
Upsert:用 page_name 當 idempotency key。內部 GET 找有沒有同 page_name 的 block
找到就 PATCH 不到就 POST 新建。解 arcrun workflow 缺 IF/branch 能力的缺口
arcrun.md P1 #1)。mira 7B.3f index-entry per-entity 維護是第一個使用者。
config_example: |
upsert_index_entry:
api_key: "{{api_key}}"
page_name: "index-{{entity}}"
parent_id: "{{mira_wiki_index_entities_id}}"
type: "index-entry"
user_id: "inkstone_mira_tools"
source: "ai-canon-wiki"
content: "{{compose_index_entry.data.text}}"
tags_json: '["mira-wiki", "ai-generated", "index"]'
@@ -1,3 +0,0 @@
module kbdb_upsert_block
go 1.21
@@ -1,280 +0,0 @@
// kbdb_upsert_block — 用 page_name 當 idempotency key 做 upsert
// 內部:GET /blocks?page_name=X → user_id filter → 找到 PATCH /blocks/:id 沒找到 POST /blocks
// 解 arcrun workflow 沒 IF/branch 能力的缺口(arcrun.md P1 #1
// 對應 SDDpolaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1
//
//go:build tinygo
package main
import (
"encoding/json"
"io"
"os"
"strconv"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
type Input struct {
KBDBUrl string `json:"kbdb_url"` // optional
APIKey string `json:"api_key"` // 必填
PageName string `json:"page_name"` // 必填,當 idempotency key
Content string `json:"content"` // 必填
Type string `json:"type"` // optional(建立時用,PATCH 時忽略)
ParentID string `json:"parent_id"` // optional(建立時用,PATCH 時忽略)
UserID string `json:"user_id"` // optional(建立時用 + lookup filter
Source string `json:"source"` // optional
TagsJSON string `json:"tags_json"` // optional(完整覆寫)
CreateOnly bool `json:"create_only"` // 2026-05-17 加:若 true + 已存在 → 不 PATCH,回 action="exists"
// 用於 stub creation 場景(避免 stub 覆寫已存在的 full wiki
}
var dummy [1]byte
func safePtr(b []byte) (uintptr, uint32) {
if len(b) == 0 {
return uintptr(unsafe.Pointer(&dummy[0])), 0
}
return uintptr(unsafe.Pointer(&b[0])), uint32(len(b))
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
func writeResult(action string, data map[string]interface{}) {
out, _ := json.Marshal(map[string]interface{}{
"success": true,
"action": action,
"data": data,
})
os.Stdout.Write(out)
}
// urlEncode:跟 kbdb_get 一致,避免引入 net/url
func urlEncode(s string) string {
var out []byte
for i := 0; i < len(s); i++ {
c := s[i]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') ||
c == '-' || c == '_' || c == '.' || c == '~' {
out = append(out, c)
} else {
const hex = "0123456789ABCDEF"
out = append(out, '%', hex[c>>4], hex[c&0x0f])
}
}
return string(out)
}
func httpCall(method, url string, headers map[string]string, body []byte) ([]byte, uint32) {
headersBytes, _ := json.Marshal(headers)
urlBytes := []byte(url)
methodBytes := []byte(method)
outBuf := make([]byte, 1<<20) // 1MB
var outLen uint32
urlPtr, urlLen := safePtr(urlBytes)
methodPtr, methodLen := safePtr(methodBytes)
headersPtr, headersLenU := safePtr(headersBytes)
bodyPtr, bodyLenU := safePtr(body)
result := hostHttpRequest(
urlPtr, urlLen,
methodPtr, methodLen,
headersPtr, headersLenU,
bodyPtr, bodyLenU,
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
return outBuf[:outLen], result
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var input Input
if err := json.Unmarshal(raw, &input); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if input.APIKey == "" {
writeError("api_key 必填")
return
}
if input.PageName == "" {
writeError("page_name 必填(upsert 的 idempotency key")
return
}
if input.Content == "" {
writeError("content 必填")
return
}
kbdbURL := input.KBDBUrl
if kbdbURL == "" {
kbdbURL = "https://kbdb.finally.click"
}
headers := map[string]string{
"Authorization": "Bearer " + input.APIKey,
}
// ── Step 1lookup by page_name ────────────────────────────────────
lookupURL := kbdbURL + "/blocks?page_name=" + urlEncode(input.PageName) +
"&limit=" + strconv.Itoa(10)
lookupResp, callResult := httpCall("GET", lookupURL, headers, nil)
if callResult != 0 {
writeError("KBDB lookup failed (host_http_request returned non-zero)")
return
}
var lookupParsed struct {
Blocks []map[string]interface{} `json:"blocks"`
Count int `json:"count"`
Error interface{} `json:"error"`
}
if err := json.Unmarshal(lookupResp, &lookupParsed); err != nil {
writeError("KBDB lookup returned non-JSON: " + string(lookupResp))
return
}
if lookupParsed.Error != nil {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": lookupParsed.Error,
"phase": "lookup",
})
os.Stdout.Write(errBytes)
return
}
// ── Step 2:找符合 user_id 的第一筆 ──────────────────────────────
var existing map[string]interface{}
for _, b := range lookupParsed.Blocks {
if input.UserID == "" {
existing = b
break
}
if uid, ok := b["user_id"].(string); ok && uid == input.UserID {
existing = b
break
}
}
// ── Step 3:分支寫入 ───────────────────────────────────────────────
postHeaders := map[string]string{
"Content-Type": "application/json",
"Authorization": "Bearer " + input.APIKey,
}
if existing != nil {
// CreateOnly 模式:已存在 → 不動,回 action="exists"(給 stub creation 用,
// 避免後續 raw 提到同 entity 時把完整 wiki 覆寫成 stub
if input.CreateOnly {
writeResult("exists", existing)
return
}
// PATCH 路徑
existingID, _ := existing["id"].(string)
if existingID == "" {
writeError("lookup 找到 block 但 id 為空")
return
}
patchBody := make(map[string]interface{})
patchBody["content"] = input.Content
if input.Source != "" {
patchBody["source"] = input.Source
}
if input.TagsJSON != "" {
// PATCH endpoint 用 tags array 不是 tags_json string
var tagsArr []string
if err := json.Unmarshal([]byte(input.TagsJSON), &tagsArr); err == nil {
patchBody["tags"] = tagsArr
}
}
patchBodyBytes, _ := json.Marshal(patchBody)
patchURL := kbdbURL + "/blocks/" + existingID
patchResp, callResult := httpCall("PATCH", patchURL, postHeaders, patchBodyBytes)
if callResult != 0 {
writeError("KBDB PATCH failed (host_http_request returned non-zero)")
return
}
var patchParsed map[string]interface{}
if err := json.Unmarshal(patchResp, &patchParsed); err != nil {
writeError("KBDB PATCH returned non-JSON: " + string(patchResp))
return
}
if _, hasErr := patchParsed["error"]; hasErr {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": patchParsed["error"],
"phase": "patch",
})
os.Stdout.Write(errBytes)
return
}
writeResult("patched", patchParsed)
return
}
// CREATE 路徑
postBody := make(map[string]interface{})
postBody["content"] = input.Content
postBody["page_name"] = input.PageName
if input.Type != "" {
postBody["type"] = input.Type
}
if input.ParentID != "" {
postBody["parent_id"] = input.ParentID
}
if input.UserID != "" {
postBody["user_id"] = input.UserID
}
if input.Source != "" {
postBody["source"] = input.Source
}
if input.TagsJSON != "" {
postBody["tags_json"] = input.TagsJSON
}
postBodyBytes, _ := json.Marshal(postBody)
postURL := kbdbURL + "/blocks"
postResp, callResult := httpCall("POST", postURL, postHeaders, postBodyBytes)
if callResult != 0 {
writeError("KBDB POST failed (host_http_request returned non-zero)")
return
}
var postParsed map[string]interface{}
if err := json.Unmarshal(postResp, &postParsed); err != nil {
writeError("KBDB POST returned non-JSON: " + string(postResp))
return
}
if _, hasErr := postParsed["error"]; hasErr {
errBytes, _ := json.Marshal(map[string]interface{}{
"success": false,
"error": postParsed["error"],
"phase": "create",
})
os.Stdout.Write(errBytes)
return
}
writeResult("created", postParsed)
}
@@ -1,67 +0,0 @@
canonical_id: "km_writer"
display_name: "KM Writer"
category: "api"
version: "v1"
wasi_target: "preview1"
stability: "floating"
runtime_compat:
- "cf-workers"
- "workerd"
constraints:
max_size_kb: 2048
max_cold_start_ms: 50
no_network_syscall: false
no_filesystem_syscall: true
io_model: "stdin_stdout_json"
input_schema:
type: object
required: [action, mira_url, token]
properties:
action:
type: string
description: "操作類型:read_journal | read_journal_date | append_journal | list_pages | read_page | write_page"
enum: [read_journal, read_journal_date, append_journal, list_pages, read_page, write_page]
mira_url:
type: string
description: "Mira 服務基礎 URL(例:https://mira.uncle6.me"
token:
type: string
description: "Mira MIRA_TOKENBearer token"
content:
type: string
description: "內容(append_journal / write_page 時必填)"
timestamp:
type: string
description: "ISO 8601 時間戳(append_journal 時選填,影響日期和時間顯示)"
date:
type: string
description: "日期 YYYY-MM-DDread_journal_date 時必填)"
name:
type: string
description: "頁面名稱(read_page / write_page 時必填)"
output_schema:
type: object
properties:
success:
type: boolean
data:
type: object
description: "Mira API 回應資料"
error:
type: string
description: "錯誤訊息(success=false 時)"
gherkin_tests:
- scenario: "缺少 action"
given: '{"mira_url":"https://mira.uncle6.me","token":"abc"}'
then_contains: '{"success":false'
- scenario: "缺少 token"
given: '{"action":"list_pages","mira_url":"https://mira.uncle6.me"}'
then_contains: '{"success":false'
tags: [km, journal, logseq, mira, knowledge-management]
description: "讀寫 Mira leo-graph 的 journals 和 pages。透過 host function 呼叫 Mira /km/* API,支援讀取、新增日誌條目,以及讀寫頁面。"
config_example: |
append_to_journal:
action: "append_journal"
mira_url: "https://mira.uncle6.me"
token: "<mira_token>"
content: "今天完成了 arcrun km_writer 元件"
-3
View File
@@ -1,3 +0,0 @@
module component
go 1.21
-177
View File
@@ -1,177 +0,0 @@
// km_writer — 讀寫 Mira leo-graphjournals + pages
// 透過 host function 呼叫 Mira /km/* API
//
//go:build tinygo
package main
import (
"encoding/json"
"fmt"
"io"
"os"
"unsafe"
)
//go:wasmimport u6u http_request
func hostHttpRequest(
urlPtr uintptr, urlLen uint32,
methodPtr uintptr, methodLen uint32,
headersPtr uintptr, headersLen uint32,
bodyPtr uintptr, bodyLen uint32,
outPtr uintptr, outLenPtr uintptr,
) uint32
// Input actions:
// read_journal — GET today's journal (requires: mira_url, token)
// read_journal_date — GET journal by date (requires: mira_url, token, date)
// append_journal — POST append entry (requires: mira_url, token, content; optional: timestamp)
// list_pages — GET all pages (requires: mira_url, token)
// read_page — GET page by name (requires: mira_url, token, name)
// write_page — PUT write page (requires: mira_url, token, name, content)
type Input struct {
Action string `json:"action"`
MiraURL string `json:"mira_url"`
Token string `json:"token"`
Content string `json:"content"`
Timestamp string `json:"timestamp"`
Date string `json:"date"`
Name string `json:"name"`
}
func main() {
raw, err := io.ReadAll(os.Stdin)
if err != nil {
writeError("failed to read stdin: " + err.Error())
return
}
var inp Input
if err := json.Unmarshal(raw, &inp); err != nil {
writeError("invalid input JSON: " + err.Error())
return
}
if inp.Action == "" {
writeError("action 必填")
return
}
if inp.MiraURL == "" {
writeError("mira_url 必填")
return
}
if inp.Token == "" {
writeError("token 必填")
return
}
authHeader := fmt.Sprintf(`{"Authorization":"Bearer %s","Content-Type":"application/json"}`, inp.Token)
switch inp.Action {
case "read_journal":
result := doRequest(inp.MiraURL+"/km/journal", "GET", authHeader, "")
os.Stdout.Write(result)
case "read_journal_date":
if inp.Date == "" {
writeError("date 必填(格式 YYYY-MM-DD")
return
}
result := doRequest(inp.MiraURL+"/km/journal/"+inp.Date, "GET", authHeader, "")
os.Stdout.Write(result)
case "append_journal":
if inp.Content == "" {
writeError("content 必填")
return
}
bodyMap := map[string]string{"content": inp.Content}
if inp.Timestamp != "" {
bodyMap["timestamp"] = inp.Timestamp
}
bodyBytes, _ := json.Marshal(bodyMap)
result := doRequest(inp.MiraURL+"/km/journal", "POST", authHeader, string(bodyBytes))
os.Stdout.Write(result)
case "list_pages":
result := doRequest(inp.MiraURL+"/km/pages", "GET", authHeader, "")
os.Stdout.Write(result)
case "read_page":
if inp.Name == "" {
writeError("name 必填")
return
}
result := doRequest(inp.MiraURL+"/km/page/"+inp.Name, "GET", authHeader, "")
os.Stdout.Write(result)
case "write_page":
if inp.Name == "" {
writeError("name 必填")
return
}
if inp.Content == "" {
writeError("content 必填")
return
}
bodyMap := map[string]string{"content": inp.Content}
bodyBytes, _ := json.Marshal(bodyMap)
result := doRequest(inp.MiraURL+"/km/page/"+inp.Name, "PUT", authHeader, string(bodyBytes))
os.Stdout.Write(result)
default:
writeError("未知 action: " + inp.Action)
}
}
func doRequest(url, method, headersJSON, body string) []byte {
urlBytes := []byte(url)
methodBytes := []byte(method)
headersBytes := []byte(headersJSON)
bodyBytes := []byte(body)
outBuf := make([]byte, 131072) // 128KB
var outLen uint32
if len(bodyBytes) == 0 {
bodyBytes = []byte{}
}
var bodyPtr uintptr
var bodyLen uint32
if len(bodyBytes) > 0 {
bodyPtr = uintptr(unsafe.Pointer(&bodyBytes[0]))
bodyLen = uint32(len(bodyBytes))
}
code := hostHttpRequest(
uintptr(unsafe.Pointer(&urlBytes[0])), uint32(len(urlBytes)),
uintptr(unsafe.Pointer(&methodBytes[0])), uint32(len(methodBytes)),
uintptr(unsafe.Pointer(&headersBytes[0])), uint32(len(headersBytes)),
bodyPtr, bodyLen,
uintptr(unsafe.Pointer(&outBuf[0])), uintptr(unsafe.Pointer(&outLen)),
)
if code != 0 {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": "HTTP request failed"})
return out
}
responseStr := string(outBuf[:outLen])
// Try to parse the response as JSON to forward it
var parsed interface{}
if err := json.Unmarshal([]byte(responseStr), &parsed); err != nil {
// Not JSON — wrap it
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": responseStr})
return out
}
// Forward the parsed response as-is, wrapped in success
out, _ := json.Marshal(map[string]interface{}{"success": true, "data": parsed})
return out
}
func writeError(msg string) {
out, _ := json.Marshal(map[string]interface{}{"success": false, "error": msg})
os.Stdout.Write(out)
}
@@ -1,74 +0,0 @@
# km-wiki-ingest — 機械式 wiki 卡片 → KBDB ingestArcrun#8 / 頂層 SDD T2T4
> Phase A 產物:機械 ingest 邏輯 + 乾跑證據 + workflow 設計。**不部署、不寫 live KBDB。**
## 解決什麼問題
把各 repo 的 `system-dev/wiki/cards/**/*.md`(人工精耕卡)**機械地**(無 LLM)灌進 leo21c KBDB
- **卡片 → base entry**`metadata.embed=true`,供語意搜尋)。
- **`## 實體` → graph node****`## 關聯` 的 typed-edge`A >> 關係 >> B`)與 `[[wikilink]]` → graph triplet**。
取代舊 `kbdb-ingest-plugin/scripts/ingest-cli.mjs``raw → Haiku → 三元組` 路:新路純解析卡片內既有結構,**決定性、零 token、零幻覺**。
## 形式選擇與理由(給總管)
**形式 = Arcrun workflowYAML 編排)+ 通用 `code` 零件(sandbox inline JSArcrun#10)承載卡片→envelope 解析 現成零件(`cron` / `http_request` / `foreach_control` / `kbdb_upsert_block`)。** 不再鑄 domain 零件 `km_wiki_card_parse`Arcrun#10 裁定:一次性解析走通用逃生口)。
理由:
1. **編排本來就是 arcrun 的主場**cron 限速 drain、Gitea webhook 只吃 delta、foreach 小批、冪等 upsert——這些跟現成零件 1:1 對得上,且 leo 要「Arcrun workflow 慢慢做」、arcrun 哲學禁一次性腳本。
2. **arcrun 唯一缺的是「卡片 → envelope」的解析**。那是一段**決定性純轉換**(無 LLM、無網路、無檔案)——正好是 `code` 零件 sandbox 的理想形狀(`stdin_stdout_json` + `no_network_syscall` + `no_filesystem_syscall`)。用通用 `code` 節點內聯這段 JS(而非鑄 domain 零件、也非在 YAML 裡塞 `string_ops` 正則):workflow 可讀、解析可單元測試、且 registry 不因一次性邏輯增生 domain 零件。
3. **小批是結構性的,不是靠祈禱**:一卡一 tick,每卡在 graph worker 的 fan-out ≈ `7+4N+M` subrequestnotes 卡 N≈4/M≈5 → est 28~33,穩壓 CF 50 頂下);`code` 節點內聯解析會**預先把超大卡以 `source_uri` anchor 分段**,任何單一 graph 呼叫都不破頂。
**Phase A 交付**:純解析+打包核心(`lib/card-to-envelope.mjs`,現在就能跑,= `code` 節點內聯 JS 的權威來源)+乾跑驗證器(`lib/dry-run.mjs`,印出「將寫入什麼」)+本 workflow.yamlparse_card = `code` 節點)。解析零件=通用 `code`Arcrun#10 分支,已就緒待部署);本 example 不再自帶 domain 零件契約。部署被閘控,故 live 接線是「設計而非執行」。
## 診斷小結:fan-out 精確來源 + 小批為何解得掉
`kbdb-graph-plugin` 現役寫入路徑(`triplet-ingest.ts` / `triplet-crud.ts` / `templates.ts` / `kbdb-client.ts`)逐行拆帳:
```
POST /triplets/ingestgraph worker 單次 invocation)對 base 的 subrequest
ensurePluginTemplates(3) # 頂層一次
+ listRecordsByTemplate(1) # 抓同 source 現存 active(冪等分組)
+ Σ_triplet [ createTriplet → ensurePluginTemplates(3) + createRecord(1) ] # ★ 每條邊重跑 ensure
+ persistNodes [ ensurePluginTemplates(3) + Σ_node createRecord(1) ]
+ Σ_deprecated updateRecord(1)
= 7 + 4*N_triplets + M_nodes + D_deprecated
```
- **精確炸點還原**07_01 單一 envelope 吞 `N=11, M=10, D=0``7+44+10 = 61 > 50` → 破頂半殘。**放大器=`createTriplet` 內每條邊都重呼 `ensurePluginTemplates`3 個 GET**,佔了 33/61。
- **小批為何解得掉**:把「整檔一 envelope」改成「一卡一 envelope、必要時再 anchor 分段」,把 `N` 壓到讓 `7+4N+M ≤ 40`。notes 三卡實測 est 上限 = 33,全綠。超大卡(自測 20 邊/22 節點=114)→ 自動分 4 段,每段 ≤ 38。
- **附帶建議(非本 Phase 必改)**:graph 端把 `createTriplet`/`persistNodes` 內重複的 `ensurePluginTemplates` 提到 ingest 入口只跑一次,可把每 envelope 省下 `3*(N+1)` 個 subrequest(單卡 est 33→約 18),批量還能更大。此為 graph-plugin 的可選優化,記此存查。
## 冪等設計
| 對象 | 冪等鍵 | 行為 |
|---|---|---|
| **entry** | `page_name`(穩定:`wikicard:<repo>/<canonical>`+ `metadata.content_hash` | 找到同 page_namehash 相同 → skip;不同 → PATCH content(觸發重嵌)。沒有 → POST 新建。 |
| **triplet envelope** | `source.uri` + `source.content_hash` | graph 現役 per-source 冪等:同 hash 整包 no-op`triplet-ingest.ts:65`)。 |
| **分段** | 各段 `source.uri = <基uri>#segNN` | 各段獨立 uri → 各自獨立冪等,**繞開 per-source content_hash 整包 skip**(否則同 uri 第 2 段起會被判定「已落地」而整包跳過)。節點只放進「首次引用它的段」,跨段不重送(避免 graph 重建 entity)。 |
## 觸發(兩階段,對齊 SDD R3)
- **Phase 0(一次性 backfill**`cron */2` 每 tick drain 一張卡(限速慢推),反覆跑到全庫清空。冪等 → 可續傳、重跑零寫入。
- **穩態(日常增量)****Gitea push webhook → arcrun workflow**,只吃 `commits[].{added,modified}` 中的 `system-dev/wiki/cards/**/*.md`。⚠️ Gitea → Cloudflare(arcrun)**非 GitHub Actions**,不觸 GitHub flag 紅線(D4/D20)。量小、不撞頂、不限速。
## 乾跑證據(Phase A,不寫 live
```
node lib/dry-run.mjs --repo-path <notes clone> --repo Leo/notes --self-test
```
`Leo/notes` 的 3 張卡實測:3 entriesembed=true+ 3 envelopes、15 triplets、16 nodes
**單次 graph 呼叫 subrequest 上限 = 33< 50),無任一 envelope 破頂**
self-test 合成超大卡(不分段 est=114 會炸)→ 自動分 4 段、每段 ≤ 38,全綠。
## 待 live 部署 + 寫入(總管過 leo 閘用)
1. **部署通用 `code` 零件**Arcrun#10 分支 `feat/issue-10-code-component`,已就緒):`cd registry/components/code && npm install && npx wrangler deploy`(→ `code.arcrun.dev`)+ `register-component.sh code`。本 workflow 的 parse_card 以 `component: code` 引用它,解析 JS 已內聯在 workflow.yaml(= `lib/card-to-envelope.mjs` 邏輯)。不再部署 domain 零件 `km_wiki_card_parse`
2. **部署 workflow**`km_wiki_ingest_drain`cron drain);`wrangler` 直推 leo21c**禁 `acr update`**——codeload 綁 GitHub 假綠,Arcrun#4)。
3. **注入環境變數**(不放 repo):`repo=Leo/notes ref=main gitea_token kbdb_url=https://arcrun-kbdb.leo21c.workers.dev kbdb_api_key graph_url graph_api_key=leo``CLOUDFLARE_ACCOUNT_ID=leo21c`(別讓官方 58309b 污染)。
4. **entry 寫入路徑確認**:若 `kbdb_upsert_block` 尚不透傳 `metadata_json`(需 `embed:true`/`content_hash`),entry 改用 `http_request` 直打 base `POST/PATCH /entries``body_json.metadata_json`
5. **預期寫入量(Leo/notes 現況 3 卡)**3 entries + 15 triplets + 16 node records(去重後更少);分 3 次 graph 呼叫(每次 ≤ 33 subrequest+ 3 次 entry upsert。全庫鋪開時照 cron 一卡一 tick 慢推。
6. **驗收**ingest 後 `GET /embed/backfill/status` 應見 pending 上升→drain 後歸零、embedded 增加;三模式(關鍵字/語意/圖)curl 驗。
@@ -1,176 +0,0 @@
{
"repo": "Leo/notes",
"commit": "4b9a53c1c99d596c23b1b449fd79728610f6995b",
"budget": 40,
"ceiling": 50,
"cards": [
{
"relPath": "system-dev/wiki/cards/notes/Gitea當後端編輯器全CF化網站構想.md",
"canonical": "Gitea當後端編輯器全CF化網站構想",
"entry": {
"page_name": "wikicard:Leo/notes/Gitea當後端編輯器全CF化網站構想",
"entry_type": "wiki_card",
"metadata.embed": true,
"content_hash": "64b402952fe0…",
"content_bytes": 2324,
"tags": [
"系統設計",
"工具教學"
]
},
"envelopeCount": 1,
"envelopes": [
{
"source.uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/Gitea當後端編輯器全CF化網站構想.md",
"source.anchor": null,
"nodes": 6,
"triplets": 5,
"est_subrequests": 33,
"under_ceiling": true,
"sample_triplets": [
"Gitea >> 類比於 >> WordPress (1)",
"Gitea >> 充當後端供稿給 >> Cloudflare Pages (1)",
"Quartz >> 目前負責轉譯給 >> Cloudflare Pages (1)",
"Cloudflare Artifacts >> 若提供 git 倉庫則可取代 >> Gitea (1)"
],
"sample_nodes": [
"Gitea — 可自架的 git 平台,編輯體驗近似 WordPress 後…",
"WordPress — 常見的內容管理後端,作為 Gitea 編輯體驗的類比對象。…",
"Cloudflare Pages — Cloudflare 的靜態站前端託管。…",
"Quartz — 目前把筆記轉成網站前端的工具。…"
]
}
]
},
{
"relPath": "system-dev/wiki/cards/notes/Prompt能力即拆解自己邏輯的能力.md",
"canonical": "Prompt能力即拆解自己邏輯的能力",
"entry": {
"page_name": "wikicard:Leo/notes/Prompt能力即拆解自己邏輯的能力",
"entry_type": "wiki_card",
"metadata.embed": true,
"content_hash": "63296a663227…",
"content_bytes": 2109,
"tags": [
"AI協作",
"工具教學",
"觀點主張"
]
},
"envelopeCount": 1,
"envelopes": [
{
"source.uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/Prompt能力即拆解自己邏輯的能力.md",
"source.anchor": null,
"nodes": 5,
"triplets": 5,
"est_subrequests": 32,
"under_ceiling": true,
"sample_triplets": [
"Prompt 能力 >> 本質上等於 >> 邏輯拆解能力 (1)",
"邏輯拆解能力 >> 產出 >> pseudo code (1)",
"pseudo code >> 足以教會 >> AI (1)",
"Prompt能力即拆解自己邏輯的能力 >> 呼應 >> 程式化邏輯可圖解任何主題不限AI (1)"
],
"sample_nodes": [
"Prompt 能力 — 把腦中意圖轉成能指揮 AI 的指令的能力。…",
"邏輯拆解能力 — 把腦中隱性流程外顯成可陳述步驟的能力。…",
"pseudo code — 用類程式的步驟描述邏輯、尚未綁定特定語法的表達。…",
"AI — 需被人以指令/範例指揮才產出的生成模型。…"
]
}
]
},
{
"relPath": "system-dev/wiki/cards/notes/程式化邏輯可圖解任何主題不限AI.md",
"canonical": "程式化邏輯可圖解任何主題不限AI",
"entry": {
"page_name": "wikicard:Leo/notes/程式化邏輯可圖解任何主題不限AI",
"entry_type": "wiki_card",
"metadata.embed": true,
"content_hash": "a9dbf5fc0f9a…",
"content_bytes": 2577,
"tags": [
"工具教學",
"觀點主張",
"系統設計"
]
},
"envelopeCount": 1,
"envelopes": [
{
"source.uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/程式化邏輯可圖解任何主題不限AI.md",
"source.anchor": null,
"nodes": 5,
"triplets": 5,
"est_subrequests": 32,
"under_ceiling": true,
"sample_triplets": [
"程式化邏輯 >> 可圖解 >> 亞洲金融風暴 (1)",
"流程圖解 >> 奠基於 >> 程式化邏輯 (1)",
"系統動力學 >> 類同於 >> 流程圖解 (1)",
"程式化邏輯可圖解任何主題不限AI >> 呼應 >> Prompt能力即拆解自己邏輯的能力 (1)"
],
"sample_nodes": [
"程式化邏輯 — 以程式的因果鏈結構來表述任一領域的邏輯。…",
"亞洲金融風暴 — 講者小 Lin 用長邏輯鏈敘述的金融事件案例。…",
"流程圖解 — 用 n8n 這類流程工具把邏輯視覺化講解的方法。…",
"系統動力學 — 以存量流量與回饋環圖解因果的建模工具。…"
]
}
]
}
],
"totals": {
"cards": 3,
"entries_to_upsert": 3,
"triplet_envelopes": 3,
"total_triplets": 15,
"total_nodes": 16,
"max_est_subrequests_single_call": 33,
"ceiling": 50,
"budget": 40,
"any_envelope_over_ceiling": 0
},
"self_test": {
"note": "合成 20 邊 / 22 節點 的超大卡",
"if_single_envelope_est_subrequests": 114,
"would_crash_single": true,
"segmented_into": 4,
"per_segment": [
{
"uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/合成超大卡.md#seg01",
"anchor": "seg01",
"triplets": 6,
"nodes": 7,
"est_subrequests": 38,
"under_ceiling": true
},
{
"uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/合成超大卡.md#seg02",
"anchor": "seg02",
"triplets": 6,
"nodes": 6,
"est_subrequests": 37,
"under_ceiling": true
},
{
"uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/合成超大卡.md#seg03",
"anchor": "seg03",
"triplets": 6,
"nodes": 6,
"est_subrequests": 37,
"under_ceiling": true
},
{
"uri": "gitea:Leo/notes@system-dev/wiki/cards/notes/合成超大卡.md#seg04",
"anchor": "seg04",
"triplets": 3,
"nodes": 3,
"est_subrequests": 22,
"under_ceiling": true
}
],
"all_segments_under_ceiling": true
}
}
@@ -1,331 +0,0 @@
// km-wiki-ingest — 機械式卡片→(entry + triplet envelope) 轉換核心(無 LLM、純函式)
// ---------------------------------------------------------------------------
// 取代舊 `kbdb-ingest-plugin/scripts/ingest-cli.mjs` 的 raw→Haiku 路:
// 舊路 = 讀裸筆記 → 呼叫 Haiku 萃 (s,p,o) → envelope(有 LLM、非決定性、耗 token)。
// 新路 = 讀「已精耕卡片」(`system-dev/wiki/cards/**/*.md`)→ 直接解析卡片內既有的
// `## 實體`(節點)、`## 關聯` 的 typed-edge`A >> 關係 >> B`)與 `[[wikilink]]`
// → entry + triplet envelope。純機械、決定性、零 token。
//
// 這支=通用 `code` 零件(Arcrun#10sandbox inline JS)承載的解析邏輯本體。
// workflow.yaml 的 parse_card 節點把本檔的 planCard 邏輯內聯進 code 零件的 config
// (去 import/export、raw NUL 分隔符改 u0000 escape、改用 code 沙箱注入的 sha256);
// 不再鑄 domain 零件 km_wiki_card_parseArcrun#10 裁定:一次性解析走通用逃生口)。
// 本檔續留作「該內聯 JS 的權威來源 + 可單元測試的參考實作」(純函式、stdin→stdout JSON、無 fs/網路)。
//
// 對齊契約:kbdb-ingest-plugin/contracts/ingest-candidate.jsonenvelope 形狀 / 禁止欄位)。
// 對齊頂層 SDD:卡片→entrymetadata.embed=true,走 base API)、wikilink→triplet(走 graph)。
//
// 鐵律:不碰儲存、不算向量、不建表。這支只「產出將寫入什麼」,實際 HTTP 由 workflow 打。
import { createHash } from 'node:crypto';
// --- CF subrequest 預算(防「Too many subrequests by single Worker invocation」,07_01 根因)---
//
// graph worker 處理一次 POST /triplets/ingest 時,對 base 的每次 fetch = 1 subrequest。
// 精確拆帳(讀 kbdb-graph-plugin/src/actions/triplet-ingest.ts + triplet-crud.ts + templates.ts):
// ingestEnvelope = ensurePluginTemplates(3) + listRecordsByTemplate(1)
// + Σ triplet [ createTriplet → ensurePluginTemplates(3) + createRecord(1) = 4 ]
// + persistNodes [ ensurePluginTemplates(3) + Σ node createRecord(1) ]
// + Σ deprecated updateRecord(1)
// ⟹ subreq(envelope) = 7 + 4*N_triplets + M_nodes + D_deprecated
//
// 07_01 實測炸點:N=11, M=10, D=0 → 7+44+10 = 61 > 50CF 免費/bundled 上限)→ 炸半殘。
//
// 對策 = 「一卡一 tick、每 envelope 壓在預算下、超大檔以 source_uri anchor 分段」。
export const SUBREQ_CEILING = 50; // CF 單次 Worker invocation subrequest 硬上限(bundled
export const SUBREQ_BUDGET = 40; // 我們的目標上限(留 10 給 D_deprecated 等變動)
/** 精確估算「一個 envelope 打進 graph /triplets/ingest」會在 graph worker 內產生幾個 subrequest。 */
export function estimateEnvelopeSubrequests(nTriplets, mNodes, dDeprecated = 0) {
return 7 + 4 * nTriplets + mNodes + dDeprecated;
}
// --- sha256content_hash 冪等鍵)---
export function sha256(text) {
return createHash('sha256').update(text).digest('hex');
}
// --- frontmatter 解析(極簡 YAML:只吃我們卡片用到的 tags / gloss / pipeline_candidate---
function parseFrontmatter(md) {
const m = md.match(/^---\n([\s\S]*?)\n---\n?/);
if (!m) return { data: {}, body: md };
const body = md.slice(m[0].length);
const data = {};
for (const line of m[1].split('\n')) {
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
if (!kv) continue;
const key = kv[1];
let val = kv[2].trim();
if (val.startsWith('[') && val.endsWith(']')) {
// inline list: [a, b, c]
data[key] = val.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
} else if (val === 'true' || val === 'false') {
data[key] = val === 'true';
} else {
data[key] = val;
}
}
return { data, body };
}
// --- 取某個 `## 標題` / `### 標題` 區塊的內文(到下一個同級或更高級標題為止)---
function sectionBody(md, heading) {
// heading 例:'## 實體'、'### 內文知識關係'
const level = heading.match(/^#+/)[0].length;
const lines = md.split('\n');
const out = [];
let inSec = false;
for (const line of lines) {
const h = line.match(/^(#+)\s+(.*)$/);
if (h) {
const thisLevel = h[1].length;
if (inSec) {
// 遇到同級或更高級標題 → 區塊結束
if (thisLevel <= level) break;
}
// 標題文字「開頭相符」即算命中(容忍標題後帶括號補述)
if (!inSec && thisLevel === level && line.replace(/^#+\s+/, '').startsWith(heading.replace(/^#+\s+/, ''))) {
inSec = true;
continue;
}
}
if (inSec) out.push(line);
}
return out.join('\n');
}
// --- 實體行解析:`- **正規名**(別名1/別名2)— 描述`(別名、描述皆選填)---
function parseEntities(md) {
const sec = sectionBody(md, '## 實體');
const entities = [];
for (const raw of sec.split('\n')) {
const line = raw.trim();
if (!line.startsWith('- ')) continue;
if (line.startsWith('- >') || line.startsWith('> ')) continue; // 跳過引言說明行
const m = line.match(/^- \*\*(.+?)\*\*(?:(.+?))?\s*(?:[—–\-]\s*(.*))?$/);
if (!m) continue;
const name = m[1].trim();
if (!name) continue;
const aliases = m[2]
? m[2].split(/[/、,]/).map((s) => s.trim()).filter((s) => s && s !== name)
: [];
const gloss = (m[3] || '').trim();
entities.push({ name, aliases, gloss });
}
return entities;
}
// --- typed-edge 行解析:`A >> 謂詞 >> B`(端點可為裸實體名或 [[wikilink]]---
function parseTypedEdges(sectionText) {
const edges = [];
for (const raw of (sectionText || '').split('\n')) {
const line = raw.trim();
if (!line.startsWith('- ')) continue;
const body = line.slice(2).trim();
if (body.startsWith('') || body.startsWith('(')) continue; // 「(暫無…)」占位行
const parts = body.split('>>');
if (parts.length !== 3) continue;
const subject = stripWikilink(parts[0].trim());
const predicate = parts[1].trim();
const object = stripWikilink(parts[2].trim());
if (!subject || !predicate || !object) continue;
edges.push({ subject, predicate, object });
}
return edges;
}
// [[notes/00-INDEX]] → notes/00-INDEX ;純字串則原樣回。
function stripWikilink(s) {
const m = s.match(/^\[\[(.+?)\]\]$/);
return m ? m[1].trim() : s;
}
// --- 抽所有 inline [[wikilink]](含 header 的 ← [[notes/00-INDEX]] 與內文)---
function extractInlineWikilinks(md) {
const out = [];
const re = /\[\[(.+?)\]\]/g;
let m;
while ((m = re.exec(md)) !== null) out.push(m[1].trim());
return out;
}
// --- 卡片 canonical id:以檔名(去副檔名)為準,對齊 `## 卡片關係` 用的 [[基名]] 慣例 ---
export function cardCanonical(relPath) {
const base = relPath.split('/').pop().replace(/\.md$/, '');
return base;
}
/**
* 解析一張卡片 → { entry, nodes, triplets, meta }(尚未分段的原始產物)。
* relPath:卡片相對 repo 根路徑(如 system-dev/wiki/cards/notes/Xxx.md)。
* repo:如 'Leo/notes'。
*/
export function parseCard(md, relPath, repo = 'Leo/notes') {
const { data: fm } = parseFrontmatter(md);
const canonical = cardCanonical(relPath);
const titleMatch = md.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : canonical;
// 1) 節點:## 實體 的正規名 + 別名 + gloss。
const entities = parseEntities(md);
// 2) 邊:內文知識關係(實體↔實體)+ 卡片關係(卡↔卡)+ inline wikilink(卡→卡 導覽/引用)。
const intraEdges = parseTypedEdges(sectionBody(md, '### 內文知識關係'))
.map((e) => ({ ...e, confidence: 1.0 }));
const cardEdges = parseTypedEdges(sectionBody(md, '### 卡片關係'))
.map((e) => ({ ...e, confidence: 1.0 }));
// inline wikilink(← [[notes/00-INDEX]] 等)→ 卡→卡「連結至」邊,去重、排除自環與已被 typed 邊覆蓋者。
const typedPairs = new Set(
[...cardEdges].map((e) => `${e.subject}${e.object}`),
);
const seenRef = new Set();
const refEdges = [];
for (const target of extractInlineWikilinks(md)) {
const t = stripWikilink(target);
if (t === canonical || t === title) continue; // 自環
if (typedPairs.has(`${canonical}${t}`)) continue; // 已有明確謂詞邊
const key = `${canonical}${t}`;
if (seenRef.has(key)) continue;
seenRef.add(key);
refEdges.push({ subject: canonical, predicate: '連結至', object: t, confidence: 0.5 });
}
const triplets = [...intraEdges, ...cardEdges, ...refEdges];
// 3) 節點清單:卡片本身(canonical,帶 frontmatter gloss+ 內文實體。
// 卡對卡邊指到的「別張卡」不在此補 node —— 那張卡自己被 ingest 時會補自己的 node。
const nodes = [];
const seenNode = new Set();
const pushNode = (n) => {
const k = n.name.toLowerCase();
if (!n.name || seenNode.has(k)) return;
seenNode.add(k);
nodes.push(n);
};
pushNode({ name: canonical, gloss: fm.gloss || '', aliases: title && title !== canonical ? [title] : [] });
for (const e of entities) pushNode({ name: e.name, gloss: e.gloss, aliases: e.aliases });
return {
entry: {
// base POST /entries(或 kbdb_upsert_block)用。metadata.embed=true → 語意可搜。
page_name: `wikicard:${repo}/${canonical}`, // idempotency key(穩定)
entry_type: 'wiki_card',
content: md, // 卡片全文逐字(embed 對象)
tags: Array.isArray(fm.tags) ? fm.tags : [],
metadata: {
embed: true, // ★ base embed 模組讀此旗標
source: `gitea:${repo}@${relPath}`,
content_hash: sha256(md),
kind: 'wiki_card',
repo,
canonical,
pipeline_candidate: fm.pipeline_candidate === true,
},
},
nodes,
triplets,
meta: { canonical, title, relPath, repo, contentHash: sha256(md) },
};
}
/**
* 把一張卡片的 (nodes, triplets) 打包成「一個或多個」ingest envelope
* 使每個 envelope 打進 graph 後的 subrequest 都 ≤ SUBREQ_BUDGET。
*
* 分段規則(對應頂層 SDD R4 / issue #8 第4點):
* - 單 envelope 夠塞(7+4N+M ≤ budget)→ 不分段,uri = 基 uri(無 anchor)。
* - 需分段 → 每段 uri = `<基uri>#seg{NN}`、anchor = `seg{NN}`。
* 每段是「獨立 source_uri」→ 各自獨立冪等,繞開 graph 的 per-source content_hash 整包 skip
* (否則同 uri 第 2 段起會被 line 65 的 content_hash 命中而整包跳過)。
* - 節點只放進「第一個引用到它的段」,跨段不重送(避免 graph persistNodes 重建 entity record)。
*/
export function planEnvelopes(parsed, opts = {}) {
const budget = opts.budget ?? SUBREQ_BUDGET;
const repo = parsed.meta.repo;
const relPath = parsed.meta.relPath;
const baseUri = `gitea:${repo}@${relPath}`;
const contentHash = parsed.meta.contentHash;
const commit = opts.commit;
const extractor = {
model: opts.extractorModel ?? 'mechanical/km-wiki-card-parse@1',
tier: 'deep', // 人工精耕卡=deep(決定性、非淺萃)
extracted_at: Math.floor(Date.now() / 1000),
};
const nodeByName = new Map(parsed.nodes.map((n) => [n.name, n]));
// 貪婪打包:逐條 triplet 累進,段成本 = 7 + 4*(段內邊數) + (段內首見節點數)。
const segments = [];
let cur = null;
const startSeg = () => {
cur = { triplets: [], nodeNames: new Set() };
segments.push(cur);
};
const segCost = (seg, extraEdges = 0, extraNodes = 0) =>
estimateEnvelopeSubrequests(seg.triplets.length + extraEdges, seg.nodeNames.size + extraNodes);
startSeg();
for (const t of parsed.triplets) {
// 這條邊會新引入哪些節點(subject/object 命中 nodeByName 且本段尚未收)
const cand = [t.subject, t.object].filter(
(nm) => nodeByName.has(nm) && !cur.nodeNames.has(nm) && !anySegHas(segments, cur, nm),
);
// 放得下?(含新增這條邊 + 新引入節點)
if (cur.triplets.length > 0 && segCost(cur, 1, cand.length) > budget) {
startSeg();
}
cur.triplets.push(t);
for (const nm of [t.subject, t.object]) {
if (nodeByName.has(nm) && !anySegHas(segments, null, nm)) cur.nodeNames.add(nm);
}
}
const multi = segments.length > 1;
const envelopes = segments.map((seg, i) => {
const anchor = multi ? `seg${String(i + 1).padStart(2, '0')}` : undefined;
const uri = multi ? `${baseUri}#${anchor}` : baseUri;
const nodes = [...seg.nodeNames].map((nm) => {
const n = nodeByName.get(nm);
const out = { name: n.name };
if (n.gloss) out.gloss = n.gloss;
if (n.aliases && n.aliases.length) out.aliases = n.aliases;
out.embed = true;
return out;
});
const source = { uri, content_hash: contentHash };
if (anchor) source.anchor = anchor;
if (commit) source.commit = commit;
return {
source,
extractor,
nodes,
triplets: seg.triplets.map((t) => ({
subject: t.subject,
predicate: t.predicate,
object: t.object,
confidence: t.confidence ?? 1.0,
})),
_estSubrequests: estimateEnvelopeSubrequests(seg.triplets.length, seg.nodeNames.size),
};
});
// triplets≥1 是契約硬性;無邊的卡不產 envelope(仍會建 entry)。
return envelopes.filter((e) => e.triplets.length >= 1);
}
function anySegHas(segments, exclude, name) {
for (const s of segments) {
if (s === exclude) continue;
if (s.nodeNames.has(name)) return true;
}
return false;
}
/** 一張卡片 → 完整 ingest 計畫(entry + envelopes)。planCard = parseCard + planEnvelopes。 */
export function planCard(md, relPath, repo = 'Leo/notes', opts = {}) {
const parsed = parseCard(md, relPath, repo);
const envelopes = planEnvelopes(parsed, opts);
return { entry: parsed.entry, envelopes, meta: parsed.meta, nodeCount: parsed.nodes.length, tripletCount: parsed.triplets.length };
}
@@ -1,181 +0,0 @@
#!/usr/bin/env node
// km-wiki-ingest 乾跑(dry-run)驗證器 — 不寫 live、不部署。
// -------------------------------------------------------------
// 註:解析在 live 由 workflow.yaml 的 parse_card = 通用 `code` 零件(sandbox inline JS)承載;
// 本驗證器直接 import card-to-envelope.mjs(=該 code 節點內聯 JS 的權威來源)跑同一份邏輯,
// 故 dry-run 的 envelope 結果與 code 節點在 live 的輸出等價(Arcrun#10 已單測證明逐欄全等)。
// 對某 repo 的 system-dev/wiki/cards/**/*.md 跑機械解析 + envelope 打包,
// 輸出「將寫入什麼」:① entry 清單(page_name / entry_type / metadata.embed / content_hash
// ② triplet envelope 清單(每段的 nodes / triplets / source.uri+anchor
// ③ 每個 graph /triplets/ingest 呼叫的 subrequest 估算(證明壓在 CF 上限下)
// ④ 冪等鍵設計(entry=page_name+content_hashtriplet=source.uri+content_hash)。
//
// 用法:node dry-run.mjs --repo-path <clone路徑> [--repo Leo/notes] [--budget 40] [--json] [--full]
// --self-test 額外跑「合成超大卡」證明分段生效。
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { planCard, estimateEnvelopeSubrequests, SUBREQ_CEILING, SUBREQ_BUDGET } from './card-to-envelope.mjs';
function parseArgs(argv) {
const out = { repo: 'Leo/notes', budget: SUBREQ_BUDGET };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--repo-path') out.repoPath = argv[++i];
else if (a === '--repo') out.repo = argv[++i];
else if (a === '--budget') out.budget = Number(argv[++i]);
else if (a === '--json') out.json = true;
else if (a === '--full') out.full = true;
else if (a === '--self-test') out.selfTest = true;
}
return out;
}
function walkCards(dir) {
const out = [];
if (!existsSync(dir)) return out;
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, e.name);
if (e.isDirectory()) out.push(...walkCards(p));
else if (e.name.endsWith('.md') && e.name !== '.gitkeep' && !e.name.startsWith('00-INDEX')) out.push(p);
}
return out;
}
function gitCommit(repoPath) {
try {
return execFileSync('git', ['-C', repoPath, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
} catch { return undefined; }
}
const args = parseArgs(process.argv.slice(2));
if (!args.repoPath) {
console.error('用法: node dry-run.mjs --repo-path <clone路徑> [--repo Leo/notes] [--budget 40] [--json] [--full] [--self-test]');
process.exit(1);
}
const cardsRoot = path.join(args.repoPath, 'system-dev', 'wiki', 'cards');
const cardPaths = walkCards(cardsRoot);
const commit = gitCommit(args.repoPath);
const report = { repo: args.repo, commit, budget: args.budget, ceiling: SUBREQ_CEILING, cards: [], totals: {} };
let totEntries = 0, totEnvelopes = 0, totTriplets = 0, totNodes = 0, maxSub = 0, over = 0;
for (const p of cardPaths) {
const rel = path.relative(args.repoPath, p);
const md = readFileSync(p, 'utf8');
const plan = planCard(md, rel, args.repo, { budget: args.budget, commit });
totEntries++;
totEnvelopes += plan.envelopes.length;
const cardTriplets = plan.envelopes.reduce((s, e) => s + e.triplets.length, 0);
const cardNodes = plan.envelopes.reduce((s, e) => s + e.nodes.length, 0);
totTriplets += cardTriplets;
totNodes += cardNodes;
for (const e of plan.envelopes) {
maxSub = Math.max(maxSub, e._estSubrequests);
if (e._estSubrequests > SUBREQ_CEILING) over++;
}
report.cards.push({
relPath: rel,
canonical: plan.meta.canonical,
entry: {
page_name: plan.entry.page_name,
entry_type: plan.entry.entry_type,
'metadata.embed': plan.entry.metadata.embed,
content_hash: plan.entry.metadata.content_hash.slice(0, 12) + '…',
content_bytes: Buffer.byteLength(plan.entry.content, 'utf8'),
tags: plan.entry.tags,
},
envelopeCount: plan.envelopes.length,
envelopes: plan.envelopes.map((e) => ({
'source.uri': e.source.uri,
'source.anchor': e.source.anchor ?? null,
nodes: e.nodes.length,
triplets: e.triplets.length,
est_subrequests: e._estSubrequests,
under_ceiling: e._estSubrequests <= SUBREQ_CEILING,
sample_triplets: e.triplets.slice(0, args.full ? 999 : 4).map((t) => `${t.subject} >> ${t.predicate} >> ${t.object} (${t.confidence})`),
sample_nodes: e.nodes.slice(0, args.full ? 999 : 4).map((n) => `${n.name}${n.gloss ? ' — ' + n.gloss.slice(0, 30) + '…' : ''}`),
})),
});
}
report.totals = {
cards: totEntries,
entries_to_upsert: totEntries,
triplet_envelopes: totEnvelopes,
total_triplets: totTriplets,
total_nodes: totNodes,
max_est_subrequests_single_call: maxSub,
ceiling: SUBREQ_CEILING,
budget: args.budget,
any_envelope_over_ceiling: over,
};
// --- self-test:合成一張「超大卡」(20 邊 + 22 節點)證明單 envelope 會爆、分段後每段壓在預算下 ---
if (args.selfTest) {
const entities = [];
const edges = [];
for (let i = 0; i < 22; i++) entities.push(`- **實體${i}**(別名${i})— 這是實體 ${i} 的一句描述。`);
for (let i = 0; i < 20; i++) edges.push(`- 實體${i} >> 關聯到 >> 實體${i + 1}`);
const bigCard = `---\ntags: [壓測]\ngloss: 合成超大卡,測分段。\n---\n# 合成超大卡\n\n← [[notes/00-INDEX]]\n\n## 實體\n${entities.join('\n')}\n\n## 關聯\n### 內文知識關係\n${edges.join('\n')}\n`;
const plan = planCard(bigCard, 'system-dev/wiki/cards/notes/合成超大卡.md', args.repo, { budget: args.budget });
const single = estimateEnvelopeSubrequests(plan.tripletCount, plan.nodeCount);
report.self_test = {
note: '合成 20 邊 / 22 節點 的超大卡',
if_single_envelope_est_subrequests: single,
would_crash_single: single > SUBREQ_CEILING,
segmented_into: plan.envelopes.length,
per_segment: plan.envelopes.map((e) => ({
uri: e.source.uri, anchor: e.source.anchor, triplets: e.triplets.length, nodes: e.nodes.length, est_subrequests: e._estSubrequests, under_ceiling: e._estSubrequests <= SUBREQ_CEILING,
})),
all_segments_under_ceiling: plan.envelopes.every((e) => e._estSubrequests <= SUBREQ_CEILING),
};
}
if (args.json) {
console.log(JSON.stringify(report, null, 2));
process.exit(0);
}
// --- 人類可讀輸出 ---
const L = (s = '') => console.log(s);
L(`\n================ km-wiki-ingest DRY-RUN(不寫 live================`);
L(`repo=${report.repo} commit=${(commit || '(none)').slice(0, 12)} budget=${args.budget} CF_ceiling=${SUBREQ_CEILING}`);
L(`卡片來源根:${path.relative(args.repoPath, cardsRoot)} 找到 ${cardPaths.length} 張卡\n`);
for (const c of report.cards) {
L(`── 卡片:${c.relPath}`);
L(` ENTRYbase POST /entries 或 kbdb_upsert_block,冪等鍵 page_name):`);
L(` page_name = ${c.entry.page_name}`);
L(` entry_type = ${c.entry.entry_type}`);
L(` metadata.embed= ${c.entry['metadata.embed']} content_hash=${c.entry.content_hash} bytes=${c.entry.content_bytes}`);
L(` tags = ${JSON.stringify(c.entry.tags)}`);
L(` TRIPLET ENVELOPE(s)POST graph /triplets/ingest;分段數=${c.envelopeCount}):`);
for (const e of c.envelopes) {
L(` • uri=${e['source.uri']}${e['source.anchor'] ? ' anchor=' + e['source.anchor'] : ''}`);
L(` nodes=${e.nodes} triplets=${e.triplets} est_subrequests=${e.est_subrequests} ≤ceiling? ${e.under_ceiling ? 'YES' : 'NO ⚠️'}`);
for (const t of e.sample_triplets) L(` - ${t}`);
if (e.sample_nodes.length) L(` nodes: ${e.sample_nodes.join(' | ')}`);
}
L('');
}
L(`================ 彙總 ================`);
for (const [k, v] of Object.entries(report.totals)) L(` ${k.padEnd(34)} = ${v}`);
L(` 冪等設計:`);
L(` entry → page_name(穩定鍵)+ metadata.content_hash(比對是否改動 → 未改 skip、改動 PATCH 重嵌)`);
L(` triplet → source.uri + source.content_hashgraph 現役 per-source 冪等;同 hash 整包 no-op`);
L(` 分段 → 各段獨立 source.uri(#segNN)→ 各自獨立冪等,繞開 per-source content_hash 整包 skip`);
if (report.self_test) {
L(`\n================ SELF-TEST:超大檔分段 ================`);
const st = report.self_test;
L(` ${st.note}`);
L(` 若不分段(單 envelope)估算 subrequest = ${st.if_single_envelope_est_subrequests} → 會炸? ${st.would_crash_single ? 'YES> ' + SUBREQ_CEILING + '' : 'no'}`);
L(` 分段後段數 = ${st.segmented_into},每段:`);
for (const s of st.per_segment) L(` - ${s.anchor}: triplets=${s.triplets} nodes=${s.nodes} est=${s.est_subrequests} ≤ceiling? ${s.under_ceiling ? 'YES' : 'NO ⚠️'}`);
L(` 全部段壓在上限下? ${st.all_segments_under_ceiling ? 'YES ✅' : 'NO ⚠️'}`);
}
L('');
@@ -1 +0,0 @@
["ingest", "kbdb", "wiki", "mechanical", "cron", "webhook", "graph", "triplet", "no-llm"]
@@ -1,458 +0,0 @@
name: km_wiki_ingest_drain
description: >
Phase 0 限速 draincron 每 tick 只處理「一張卡」→ 機械解析成 entry + triplet envelope
→ 冪等寫 KBDBbase entry / graph triplet)。反覆跑直到全庫 drain 完。
來源=repo 的 system-dev/wiki/cards/**/*.md(人工精耕卡,非裸筆記,無 LLM)。
解析由通用 code 零件(sandbox inline JS)承載,不再鑄 domain 零件(Arcrun#10 裁定)。
穩態(Gitea push webhook 只處理 delta)見檔尾 §穩態變體。
# ── 為什麼「一 tick 一卡」=根治 07_01 的 Too many subrequests ──
# graph worker 處理一次 POST /triplets/ingest 的 subrequest = 7 + 4*N_triplets + M_nodes + D_deprecated。
# 07_01 炸點:單一 envelope 吞整檔 N=11,M=10 → 61 > 50CF bundled 上限)→ 半殘。
# 對策:① 一卡一 tick(天然小批,notes 卡 ~N4/M5 → est 28~33,穩壓 50 下)
# ② code 節點的內聯解析會自動把超大卡以 source_uri anchor 分段(每段獨立冪等)。
# ⟹ 任何單一 graph 呼叫都不會再破頂。
flow:
- "watch_cron >> ON_SUCCESS >> pick_next_card"
- "pick_next_card >> ON_SUCCESS >> fetch_card"
- "fetch_card >> ON_SUCCESS >> parse_card"
- "parse_card >> ON_SUCCESS >> upsert_entry" # 卡片 → base entryembed=true),冪等
- "upsert_entry >> ON_SUCCESS >> post_envelopes" # wikilink/typed-edge → graph triplet
- "post_envelopes >> 對每個 envelope >> post_one_envelope" # 分段時多段,各段獨立冪等
config:
# 1) 排程 tick:慢推。每 2 分鐘一張卡=限速(Phase 0 唯一需要 rate-limit 之處)。
watch_cron:
component: cron
cron_expr: "*/2 * * * *"
description: "每 2 分鐘 drain 一張卡(限速慢推,避免 CF 額度與 subrequest 壓力)"
# 2) 取下一張待處理卡(cursor drain)。用 Gitea contents API 列 cards 目錄 + 一個游標 block
# 記「處理到哪」。回傳單一 { rel_path, download_url, content_hash?(git blob sha) }。
# 註:list + cursor 的細節可用 http_request(Gitea API) + set/string_ops 組;此處給語意佔位。
pick_next_card:
component: http_request
method: GET
url: "https://git.uncle6.me/api/v1/repos/{{repo}}/contents/system-dev/wiki/cards?ref={{ref}}"
headers:
Authorization: "token {{gitea_token}}"
Accept: "application/json"
# 下游用 filter/set 取「游標之後第一張、且 .md、且非 00-INDEX」的一張。
# 3) 抓卡片全文(Gitea raw)。
fetch_card:
component: http_request
method: GET
url: "{{pick_next_card.next.download_url}}"
headers:
Authorization: "token {{gitea_token}}"
# 4) ★ 機械解析 —— 通用 code 零件(sandbox inline JS,無 LLM、無 fs/網路,stdin→stdout JSON)。
# Arcrun#10 裁定:一次性解析邏輯走通用逃生口,不再鑄 domain 零件 km_wiki_card_parse。
# 下面 code: 內聯的即 lib/card-to-envelope.mjs 的 planCard 邏輯(去 import/export、
# raw NUL 分隔符改 \u0000 escape、改用 code 沙箱注入的 curated builtin sha256
# 已單測證明與原模組輸出逐欄全等)。
# input:卡片全文 md + 相對路徑 relPath + repo + opts.budgetsubrequest 目標上限)。
# output{ success:true, data:{ entry, envelopes[], meta, nodeCount, tripletCount } }
# —— envelope 已分段、已估 subrequest。故下游改引用 parse_card.data.*。
parse_card:
component: code
code: |
// km-wiki-ingest — 機械式卡片→(entry + triplet envelope) 轉換核心(無 LLM、純函式)
// ---------------------------------------------------------------------------
// 取代舊 `kbdb-ingest-plugin/scripts/ingest-cli.mjs` 的 raw→Haiku 路:
// 舊路 = 讀裸筆記 → 呼叫 Haiku 萃 (s,p,o) → envelope(有 LLM、非決定性、耗 token)。
// 新路 = 讀「已精耕卡片」(`system-dev/wiki/cards/**/*.md`)→ 直接解析卡片內既有的
// `## 實體`(節點)、`## 關聯` 的 typed-edge`A >> 關係 >> B`)與 `[[wikilink]]`
// → entry + triplet envelope。純機械、決定性、零 token。
//
// 這支=通用 `code` 零件(Arcrun#10sandbox inline JS)承載的解析邏輯本體。
// workflow.yaml 的 parse_card 節點把本檔的 planCard 邏輯內聯進 code 零件的 config
// (去 import/export、raw NUL 分隔符改 u0000 escape、改用 code 沙箱注入的 sha256);
// 不再鑄 domain 零件 km_wiki_card_parseArcrun#10 裁定:一次性解析走通用逃生口)。
// 本檔續留作「該內聯 JS 的權威來源 + 可單元測試的參考實作」(純函式、stdin→stdout JSON、無 fs/網路)。
//
// 對齊契約:kbdb-ingest-plugin/contracts/ingest-candidate.jsonenvelope 形狀 / 禁止欄位)。
// 對齊頂層 SDD:卡片→entrymetadata.embed=true,走 base API)、wikilink→triplet(走 graph)。
//
// 鐵律:不碰儲存、不算向量、不建表。這支只「產出將寫入什麼」,實際 HTTP 由 workflow 打。
// (import 移除:code 沙箱提供注入的 sha256 builtin)
// --- CF subrequest 預算(防「Too many subrequests by single Worker invocation」,07_01 根因)---
//
// graph worker 處理一次 POST /triplets/ingest 時,對 base 的每次 fetch = 1 subrequest。
// 精確拆帳(讀 kbdb-graph-plugin/src/actions/triplet-ingest.ts + triplet-crud.ts + templates.ts):
// ingestEnvelope = ensurePluginTemplates(3) + listRecordsByTemplate(1)
// + Σ triplet [ createTriplet → ensurePluginTemplates(3) + createRecord(1) = 4 ]
// + persistNodes [ ensurePluginTemplates(3) + Σ node createRecord(1) ]
// + Σ deprecated updateRecord(1)
// ⟹ subreq(envelope) = 7 + 4*N_triplets + M_nodes + D_deprecated
//
// 07_01 實測炸點:N=11, M=10, D=0 → 7+44+10 = 61 > 50CF 免費/bundled 上限)→ 炸半殘。
//
// 對策 = 「一卡一 tick、每 envelope 壓在預算下、超大檔以 source_uri anchor 分段」。
const SUBREQ_CEILING = 50; // CF 單次 Worker invocation subrequest 硬上限(bundled
const SUBREQ_BUDGET = 40; // 我們的目標上限(留 10 給 D_deprecated 等變動)
/** 精確估算「一個 envelope 打進 graph /triplets/ingest」會在 graph worker 內產生幾個 subrequest。 */
function estimateEnvelopeSubrequests(nTriplets, mNodes, dDeprecated = 0) {
return 7 + 4 * nTriplets + mNodes + dDeprecated;
}
// --- sha256content_hash 冪等鍵)---
// (sha256 移除:使用 code 沙箱注入的 curated builtin sha256)
// --- frontmatter 解析(極簡 YAML:只吃我們卡片用到的 tags / gloss / pipeline_candidate---
function parseFrontmatter(md) {
const m = md.match(/^---\n([\s\S]*?)\n---\n?/);
if (!m) return { data: {}, body: md };
const body = md.slice(m[0].length);
const data = {};
for (const line of m[1].split('\n')) {
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
if (!kv) continue;
const key = kv[1];
let val = kv[2].trim();
if (val.startsWith('[') && val.endsWith(']')) {
// inline list: [a, b, c]
data[key] = val.slice(1, -1).split(',').map((s) => s.trim()).filter(Boolean);
} else if (val === 'true' || val === 'false') {
data[key] = val === 'true';
} else {
data[key] = val;
}
}
return { data, body };
}
// --- 取某個 `## 標題` / `### 標題` 區塊的內文(到下一個同級或更高級標題為止)---
function sectionBody(md, heading) {
// heading 例:'## 實體'、'### 內文知識關係'
const level = heading.match(/^#+/)[0].length;
const lines = md.split('\n');
const out = [];
let inSec = false;
for (const line of lines) {
const h = line.match(/^(#+)\s+(.*)$/);
if (h) {
const thisLevel = h[1].length;
if (inSec) {
// 遇到同級或更高級標題 → 區塊結束
if (thisLevel <= level) break;
}
// 標題文字「開頭相符」即算命中(容忍標題後帶括號補述)
if (!inSec && thisLevel === level && line.replace(/^#+\s+/, '').startsWith(heading.replace(/^#+\s+/, ''))) {
inSec = true;
continue;
}
}
if (inSec) out.push(line);
}
return out.join('\n');
}
// --- 實體行解析:`- **正規名**(別名1/別名2)— 描述`(別名、描述皆選填)---
function parseEntities(md) {
const sec = sectionBody(md, '## 實體');
const entities = [];
for (const raw of sec.split('\n')) {
const line = raw.trim();
if (!line.startsWith('- ')) continue;
if (line.startsWith('- >') || line.startsWith('> ')) continue; // 跳過引言說明行
const m = line.match(/^- \*\*(.+?)\*\*(?:(.+?))?\s*(?:[—–\-]\s*(.*))?$/);
if (!m) continue;
const name = m[1].trim();
if (!name) continue;
const aliases = m[2]
? m[2].split(/[/、,]/).map((s) => s.trim()).filter((s) => s && s !== name)
: [];
const gloss = (m[3] || '').trim();
entities.push({ name, aliases, gloss });
}
return entities;
}
// --- typed-edge 行解析:`A >> 謂詞 >> B`(端點可為裸實體名或 [[wikilink]]---
function parseTypedEdges(sectionText) {
const edges = [];
for (const raw of (sectionText || '').split('\n')) {
const line = raw.trim();
if (!line.startsWith('- ')) continue;
const body = line.slice(2).trim();
if (body.startsWith('') || body.startsWith('(')) continue; // 「(暫無…)」占位行
const parts = body.split('>>');
if (parts.length !== 3) continue;
const subject = stripWikilink(parts[0].trim());
const predicate = parts[1].trim();
const object = stripWikilink(parts[2].trim());
if (!subject || !predicate || !object) continue;
edges.push({ subject, predicate, object });
}
return edges;
}
// [[notes/00-INDEX]] → notes/00-INDEX ;純字串則原樣回。
function stripWikilink(s) {
const m = s.match(/^\[\[(.+?)\]\]$/);
return m ? m[1].trim() : s;
}
// --- 抽所有 inline [[wikilink]](含 header 的 ← [[notes/00-INDEX]] 與內文)---
function extractInlineWikilinks(md) {
const out = [];
const re = /\[\[(.+?)\]\]/g;
let m;
while ((m = re.exec(md)) !== null) out.push(m[1].trim());
return out;
}
// --- 卡片 canonical id:以檔名(去副檔名)為準,對齊 `## 卡片關係` 用的 [[基名]] 慣例 ---
function cardCanonical(relPath) {
const base = relPath.split('/').pop().replace(/\.md$/, '');
return base;
}
/**
* 解析一張卡片 → { entry, nodes, triplets, meta }(尚未分段的原始產物)。
* relPath:卡片相對 repo 根路徑(如 system-dev/wiki/cards/notes/Xxx.md)。
* repo:如 'Leo/notes'。
*/
function parseCard(md, relPath, repo = 'Leo/notes') {
const { data: fm } = parseFrontmatter(md);
const canonical = cardCanonical(relPath);
const titleMatch = md.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1].trim() : canonical;
// 1) 節點:## 實體 的正規名 + 別名 + gloss。
const entities = parseEntities(md);
// 2) 邊:內文知識關係(實體↔實體)+ 卡片關係(卡↔卡)+ inline wikilink(卡→卡 導覽/引用)。
const intraEdges = parseTypedEdges(sectionBody(md, '### 內文知識關係'))
.map((e) => ({ ...e, confidence: 1.0 }));
const cardEdges = parseTypedEdges(sectionBody(md, '### 卡片關係'))
.map((e) => ({ ...e, confidence: 1.0 }));
// inline wikilink(← [[notes/00-INDEX]] 等)→ 卡→卡「連結至」邊,去重、排除自環與已被 typed 邊覆蓋者。
const typedPairs = new Set(
[...cardEdges].map((e) => `${e.subject}\u0000${e.object}`),
);
const seenRef = new Set();
const refEdges = [];
for (const target of extractInlineWikilinks(md)) {
const t = stripWikilink(target);
if (t === canonical || t === title) continue; // 自環
if (typedPairs.has(`${canonical}\u0000${t}`)) continue; // 已有明確謂詞邊
const key = `${canonical}\u0000${t}`;
if (seenRef.has(key)) continue;
seenRef.add(key);
refEdges.push({ subject: canonical, predicate: '連結至', object: t, confidence: 0.5 });
}
const triplets = [...intraEdges, ...cardEdges, ...refEdges];
// 3) 節點清單:卡片本身(canonical,帶 frontmatter gloss+ 內文實體。
// 卡對卡邊指到的「別張卡」不在此補 node —— 那張卡自己被 ingest 時會補自己的 node。
const nodes = [];
const seenNode = new Set();
const pushNode = (n) => {
const k = n.name.toLowerCase();
if (!n.name || seenNode.has(k)) return;
seenNode.add(k);
nodes.push(n);
};
pushNode({ name: canonical, gloss: fm.gloss || '', aliases: title && title !== canonical ? [title] : [] });
for (const e of entities) pushNode({ name: e.name, gloss: e.gloss, aliases: e.aliases });
return {
entry: {
// base POST /entries(或 kbdb_upsert_block)用。metadata.embed=true → 語意可搜。
page_name: `wikicard:${repo}/${canonical}`, // idempotency key(穩定)
entry_type: 'wiki_card',
content: md, // 卡片全文逐字(embed 對象)
tags: Array.isArray(fm.tags) ? fm.tags : [],
metadata: {
embed: true, // ★ base embed 模組讀此旗標
source: `gitea:${repo}@${relPath}`,
content_hash: sha256(md),
kind: 'wiki_card',
repo,
canonical,
pipeline_candidate: fm.pipeline_candidate === true,
},
},
nodes,
triplets,
meta: { canonical, title, relPath, repo, contentHash: sha256(md) },
};
}
/**
* 把一張卡片的 (nodes, triplets) 打包成「一個或多個」ingest envelope
* 使每個 envelope 打進 graph 後的 subrequest 都 ≤ SUBREQ_BUDGET。
*
* 分段規則(對應頂層 SDD R4 / issue #8 第4點):
* - 單 envelope 夠塞(7+4N+M ≤ budget)→ 不分段,uri = 基 uri(無 anchor)。
* - 需分段 → 每段 uri = `<基uri>#seg{NN}`、anchor = `seg{NN}`。
* 每段是「獨立 source_uri」→ 各自獨立冪等,繞開 graph 的 per-source content_hash 整包 skip
* (否則同 uri 第 2 段起會被 line 65 的 content_hash 命中而整包跳過)。
* - 節點只放進「第一個引用到它的段」,跨段不重送(避免 graph persistNodes 重建 entity record)。
*/
function planEnvelopes(parsed, opts = {}) {
const budget = opts.budget ?? SUBREQ_BUDGET;
const repo = parsed.meta.repo;
const relPath = parsed.meta.relPath;
const baseUri = `gitea:${repo}@${relPath}`;
const contentHash = parsed.meta.contentHash;
const commit = opts.commit;
const extractor = {
model: opts.extractorModel ?? 'mechanical/km-wiki-card-parse@1',
tier: 'deep', // 人工精耕卡=deep(決定性、非淺萃)
extracted_at: Math.floor(Date.now() / 1000),
};
const nodeByName = new Map(parsed.nodes.map((n) => [n.name, n]));
// 貪婪打包:逐條 triplet 累進,段成本 = 7 + 4*(段內邊數) + (段內首見節點數)。
const segments = [];
let cur = null;
const startSeg = () => {
cur = { triplets: [], nodeNames: new Set() };
segments.push(cur);
};
const segCost = (seg, extraEdges = 0, extraNodes = 0) =>
estimateEnvelopeSubrequests(seg.triplets.length + extraEdges, seg.nodeNames.size + extraNodes);
startSeg();
for (const t of parsed.triplets) {
// 這條邊會新引入哪些節點(subject/object 命中 nodeByName 且本段尚未收)
const cand = [t.subject, t.object].filter(
(nm) => nodeByName.has(nm) && !cur.nodeNames.has(nm) && !anySegHas(segments, cur, nm),
);
// 放得下?(含新增這條邊 + 新引入節點)
if (cur.triplets.length > 0 && segCost(cur, 1, cand.length) > budget) {
startSeg();
}
cur.triplets.push(t);
for (const nm of [t.subject, t.object]) {
if (nodeByName.has(nm) && !anySegHas(segments, null, nm)) cur.nodeNames.add(nm);
}
}
const multi = segments.length > 1;
const envelopes = segments.map((seg, i) => {
const anchor = multi ? `seg${String(i + 1).padStart(2, '0')}` : undefined;
const uri = multi ? `${baseUri}#${anchor}` : baseUri;
const nodes = [...seg.nodeNames].map((nm) => {
const n = nodeByName.get(nm);
const out = { name: n.name };
if (n.gloss) out.gloss = n.gloss;
if (n.aliases && n.aliases.length) out.aliases = n.aliases;
out.embed = true;
return out;
});
const source = { uri, content_hash: contentHash };
if (anchor) source.anchor = anchor;
if (commit) source.commit = commit;
return {
source,
extractor,
nodes,
triplets: seg.triplets.map((t) => ({
subject: t.subject,
predicate: t.predicate,
object: t.object,
confidence: t.confidence ?? 1.0,
})),
_estSubrequests: estimateEnvelopeSubrequests(seg.triplets.length, seg.nodeNames.size),
};
});
// triplets≥1 是契約硬性;無邊的卡不產 envelope(仍會建 entry)。
return envelopes.filter((e) => e.triplets.length >= 1);
}
function anySegHas(segments, exclude, name) {
for (const s of segments) {
if (s === exclude) continue;
if (s.nodeNames.has(name)) return true;
}
return false;
}
/** 一張卡片 → 完整 ingest 計畫(entry + envelopes)。planCard = parseCard + planEnvelopes。 */
function planCard(md, relPath, repo = 'Leo/notes', opts = {}) {
const parsed = parseCard(md, relPath, repo);
const envelopes = planEnvelopes(parsed, opts);
return { entry: parsed.entry, envelopes, meta: parsed.meta, nodeCount: parsed.nodes.length, tripletCount: parsed.triplets.length };
}
// graph /triplets/ingest 是 strict schema,拒絕未知鍵。planEnvelopes 於 envelope 上掛的
// 診斷鍵 _estSubrequests 不在 ingest-candidate 契約內 → post 前剝除所有 _-前綴鍵,
// 讓 post_one_envelope 的 body_json={{envelope}} 為契約乾淨 payload。
// leo21c 實測:不剝除會回 422 unrecognized_keys "_estSubrequests"。)
const __plan = planCard(input.md, input.relPath, input.repo, input.opts || {});
__plan.envelopes = __plan.envelopes.map(function (e) {
const clean = {};
for (const k in e) { if (k.charAt(0) !== '_') clean[k] = e[k]; }
return clean;
});
return __plan;
input:
md: "{{fetch_card.data.body}}"
relPath: "{{pick_next_card.next.rel_path}}"
repo: "{{repo}}"
opts:
budget: 40 # subrequest 目標上限(留 10 給 D_deprecated),超過自動 anchor 分段
limits:
timeout_ms: 3000 # 純 CPU 解析;大卡也充裕
max_output_bytes: 4194304 # envelope 陣列可能較大(4 MiB
# 5) 卡片 → base entry,冪等 upsertpage_name 當鍵;找到 PATCH、沒有 POST)。
# metadata.embed=true → base embed 模組會補嵌 → 語意可搜。
upsert_entry:
component: kbdb_upsert_block
api_key: "{{kbdb_api_key}}"
kbdb_url: "{{kbdb_url}}"
page_name: "{{parse_card.data.entry.page_name}}"
type: "{{parse_card.data.entry.entry_type}}"
content: "{{parse_card.data.entry.content}}"
source: "{{parse_card.data.entry.metadata.source}}"
tags_json: "{{parse_card.data.entry.tags_json}}"
# ⚠️ metadata.embed=true / content_hash 需經 base /entries 帶 metadata_json 落地;
# 若 kbdb_upsert_block 尚未透傳 metadata_json,改用 http_request 直打 base POST/PATCH /entries
# 帶 body_json.metadata_json(見 description.md §entry 冪等)。
# 6) triplet envelope(可能多段)→ 逐段 POST graph /triplets/ingest。
# graph 端 per-source(uri+content_hash) 冪等:同 hash 整包 no-op;分段各段 uri 不同 → 各自獨立冪等。
post_envelopes:
component: foreach_control
items: "{{parse_card.data.envelopes}}"
item_key: envelope
post_one_envelope:
component: http_request
method: POST
url: "{{graph_url}}/triplets/ingest"
headers:
Content-Type: "application/json"
X-Arcrun-API-Key: "{{graph_api_key}}"
body_json: "{{envelope}}" # envelope 已符合 ingest-candidate.json 契約(禁止欄位已排除)
# ── 執行環境變數(部署時注入;此檔不放密鑰)──
# repo=Leo/notes ref=main gitea_token=<GITEA_TOKEN>
# kbdb_url=https://arcrun-kbdb.leo21c.workers.dev kbdb_api_key=<partner key>
# graph_url=<graph plugin base url on leo21c> graph_api_key=leo
#
# ═══════════════════════════════════════════════════════════════════════════
# §穩態變體(km_wiki_ingest_delta):Gitea push webhook → 只處理 delta 檔
# ═══════════════════════════════════════════════════════════════════════════
# 觸發 = Gitea repo Settings → Webhooks → 指向 arcrun(cypher-executor) 的 workflow webhook URL。
# ⚠️ 這是 Gitea → Cloudflare(arcrun),非 GitHub Actions → 不觸 GitHub flag 紅線(D4/D20)。
# 只把上面 flow 的 watch_cron/pick_next_card 換成:
# inputwebhook payload>> ON_SUCCESS >> collect_changed
# collect_changed = 從 payload.commits[].{added,modified} 濾出 system-dev/wiki/cards/**/*.md
# >> foreach 檔 >> fetch_card >> parse_card >> upsert_entry >> post_envelopes(同上)
# 量小、天生不撞頂、不需限速;靠 graph/entry 冪等自動 skip 未變檔。
+63
View File
@@ -0,0 +1,63 @@
# Skill: INDEXArcrun 導航:什麼問題查哪裡)
> **這支的定位=LLM wiki 的 `INDEX.md`**leo 2026-07-30 點破:
> 「整個 arcrun instruction 很像 LLM wiki…它會拿到一個 index 把所有文件說明都塞給它,
> 它不會就可以查,範本全部寫在裡面」)。
>
> **push vs pull**(照 wiki 的分法):
> - **push**MCP 連線時的 `instructions`(AI 一定看到,不看就出事的三句)
> - **pull**=本檔。AI 卡住時 `arcrun_get_skill('INDEX')` 拿到全館導航
---
## 一、我現在該查哪個?(照症狀找)
| 你的處境 | 用這個 | 工具 |
|---|---|---|
| **要開始寫 workflow,但不知道有什麼零件** | `write_intent_workflow` | `arcrun_get_skill('write_intent_workflow')` |
| 想「每 X 分鐘掃 Y,找到就處理」 | `build_watcher_workflow` | `arcrun_get_skill('build_watcher_workflow')` |
| 要做檢索問答(RAG | `rag_with_arcrun` | `arcrun_get_skill('rag_with_arcrun')` |
| workflow 卡住不動/paused | `debug_paused_workflow` | `arcrun_get_skill('debug_paused_workflow')` |
| 想把 http 呼叫改成觸發別的 workflow | `migrate_http_to_trigger_workflow` | 同上 |
| **真的需要新零件**(罕見)| `add_new_wasm_component` | 同上 ⚠️ 先確認工作流做不到 |
## 二、我要查「有沒有現成的東西」
| 找什麼 | 工具 | 注意 |
|---|---|---|
| 有哪些**零件** | `arcrun_list_components()` / `arcrun_search_components('自然語言')` | 零件=能力(`http_request``code``if_control`…)|
| 有哪些 **recipe** | `arcrun_recipe_list()` / `arcrun_recipe_search('...')` | recipe=打某個 API 的配方(`telegram_send``kbdb_get`…)|
| 有哪些**跑過的 workflow** | `arcrun_list_workflows()` / `arcrun_search_workflows('...')` | **這些是最可靠的範本**(實跑過)|
| 某個 workflow 的完整定義 | `arcrun_get_workflow(name)` | 拿來照抄結構 |
| 執行紀錄/為什麼失敗 | `arcrun_list_recent_executions()` / `arcrun_get_execution_trace(id)` | |
⚠️ **零件 vs recipe 分不清會寫錯**
`telegram_send``gmail``kbdb_get`**recipe 不是零件**
它們要寫成 `http_request` 該 recipe。
## 三、已知的坑(不看會踩)
| 坑 | 現況 | 怎麼避 |
|---|---|---|
| **`/cypher/search` 回假 `found`** | 對**任何**節點名都回 found(不查 registry| status 目前不可信,改用 `list_components` 自己確認。修復中(CP `arcrun-usable` 步驟 3|
| **引擎沒有條件分支** | `grep ON_TRUE\|ON_FALSE` = 0`if_control` 只回 boolean | 判斷寫成獨立節點接 `ON_SUCCESS`。見 Gitea Arcrun#5 |
| **`registry/examples/` 8/13 是壞的** | 引用不存在的零件(把 recipe 當零件寫)| **別照抄 examples**,改用 `arcrun_get_workflow` 拿實跑過的 |
| **registry 可能是空的** | 安裝器無註冊步驟 ⇒ 新實例查不到零件 | 查不到 ≠ 不存在,別據此改寫成 code |
## 四、最重要的一條紅線
🔴 **查不到零件就改寫成 `code` 節點=「腹語術」**(表面用 Arcrun、實際全寫 JS)。
- 缺 API → **寫 recipe**`arcrun_recipe_push`
- 缺能力 → **投稿零件 PR**(要人類確認,見 `add_new_wasm_component`
- `code` 只用於**局部整形**(例:剝掉 LLM 回應的雜訊、切段落)
> 實錄:2026-07-30 盤點發現正式 workflow 只用 2 個零件,8 個 code 節點含 if×61 for×23
> 最大一個 5509 字元——**每一個 if 都是沒被測過的新 bug**。
> 零件的價值是「被測過 1000 次」,寫進 code 就歸零。
## 五、還是不會?
- `arcrun_list_skills()` — 看全部 skill
- `arcrun_get_gui_context()` — 看目前實例的狀態
- `arcrun_report_feedback()`**回報這裡沒寫清楚的地方**(這份 INDEX 該被你的問題改進)
+153
View File
@@ -0,0 +1,153 @@
# Skill: Write Intent Workflow(寫意圖工作流)
## 何時用這個 skill
**用戶說「幫我用 Arcrun 做 X」時,第一個讀這支。** 其他 skill 都是它的下游。
- 「幫我用 Arcrun 寫一個…」
- 「用 Arcrun 做 X」
- 你要在 Arcrun 上做任何事,但不知道有哪些零件可用
> **你不需要知道有哪些零件。** 先把意圖寫出來丟去查,系統會告訴你哪些存在、哪些不存在。
## 核心 pattern
```
input >> ON_SUCCESS >> <第一步> >> ... → 丟 /cypher/search → 系統回哪些零件存在
```
---
## 1. 意圖工作流的語法
一串「誰接誰」,每行一個關係:
```
<節點A> >> <邊> >> <節點B>
```
- **節點**=一個步驟。用你想得到的名字(中文可以),**不必是真實零件名**
- **邊**=什麼情況下往下走
## 2. 邊只有兩種(真範本裡出現過的)
| 邊 | 意思 | 真例 |
|---|---|---|
| `ON_SUCCESS` | 上一步成功就往下 | `input >> ON_SUCCESS >> prep` |
| `對每個 <變數>` | 上一步產出清單,逐項處理(FOREACH)| `parse_card >> 對每個 block >> post_block` |
⚠️ **不要寫 `ON_FAILURE``ON_TRUE``ON_FALSE`**——引擎目前**沒有條件分支**
(實測 `grep ON_TRUE|ON_FALSE` 於 cypher-executor = 0;見 Gitea Arcrun#5)。
需要判斷時:**寫成一個獨立節點**(例 `check_amount`)再接 `ON_SUCCESS`
讓查詢告訴你有沒有零件可用。
## 3. 第一個節點固定是 `input`
所有真範本都以 `input` 起頭——那是「觸發時帶進來的資料」。
---
## 4. 真範本(照抄結構、改內容)
> 以下四份**全部是實際部署且 `verdict=success` 的 workflow**,不是簡化示範。
> 用 `arcrun_get_workflow(<name>)` 可以拿完整定義。
### A. 最短:取資料 → 處理 `graph_neighbors`
```
input >> ON_SUCCESS >> fetch_triplets
fetch_triplets >> ON_SUCCESS >> bfs_neighbors
```
### B. 長鏈:多次查詢 → 組裝 → 問 AI → 收尾 `rag_chat`
```
input >> ON_SUCCESS >> prep
prep >> ON_SUCCESS >> kw_search
kw_search >> ON_SUCCESS >> sem_search
sem_search >> ON_SUCCESS >> fetch_triplets
fetch_triplets >> ON_SUCCESS >> fetch_blocks_a
fetch_blocks_a >> ON_SUCCESS >> assemble
assemble >> ON_SUCCESS >> ask_llm
ask_llm >> ON_SUCCESS >> finalize
```
`prep` 前處理/`assemble` 組 prompt`finalize` 收拾回應——三個常見的整形節點。
### C. 一節點分岔兩條 FOREACH `rag_ingest_card`
```
input >> ON_SUCCESS >> parse_card
parse_card >> 對每個 block >> post_block
parse_card >> 對每個 rel >> post_triplet
```
同一節點可有多條出邊,各自處理不同清單。
### D. 混合:直線 兩段 FOREACH `rag_takedown_direct`
```
input >> ON_SUCCESS >> prep
prep >> ON_SUCCESS >> list_dead_blocks
list_dead_blocks >> ON_SUCCESS >> build_deprecations
build_deprecations >> 對每個 dead_entry >> deprecate_entry
build_deprecations >> ON_SUCCESS >> list_triplets
list_triplets >> ON_SUCCESS >> pick_dead_triplets
pick_dead_triplets >> 對每個 dead_record >> deprecate_triplet
```
`build_deprecations` 同時有 FOREACH 出邊與 `ON_SUCCESS` 出邊——
前者處理清單、後者繼續主線。
---
## 5. 節點怎麼命名(照真範本的模式,查詢較容易媒合)
| 意圖 | 模式 | 真例 |
|---|---|---|
| 前處理/正規化 | `prep` | `rag_chat.prep` |
| 取一批資料 | `fetch_*``list_*` | `fetch_triplets``list_dead_blocks` |
| 搜尋 | `*_search` | `kw_search``sem_search` |
| 解析/切塊 | `parse_*` | `parse_card` |
| 寫入 | `post_*` | `post_block``post_triplet` |
| 組裝 | `assemble``build_*` | `assemble``build_deprecations` |
| 問 AI | `ask_llm` | `rag_chat.ask_llm` |
| 收尾整形 | `finalize` | `rag_chat.finalize` |
---
## 6. 寫完一定要查(**不要直接部署**)
```bash
curl -s -X POST https://arcrun-cypher-executor.<subdomain>.workers.dev/cypher/search \
-H 'content-type: application/json' -H 'X-Arcrun-API-Key: <namespace>' \
-d '{"triplets":["input >> ON_SUCCESS >> fetch_data","fetch_data >> ON_SUCCESS >> notify"]}'
```
回應的每個節點會有:
| status | 意思 | 你該做什麼 |
|---|---|---|
| `found` | 有零件,附 `input_schema`(怎麼填 payload)與 `success_rate` | **只填 payload** |
| `missing` | 沒有這個零件 | 缺 API → 寫 recipe;缺能力 → 投稿零件 PR |
| `unknown` | 查不到 registry | **不代表不存在**,別據此改寫成 code |
⚠️ **2026-07-30 已知限制**`/cypher/search` 目前對**任何**節點名都回 `found`
(不查 registry)⇒ **這個 status 現在不可信**。修復中(CP `arcrun-usable` 步驟 3)。
在它修好前:用 `arcrun_list_components` / `arcrun_search_components` 自己確認零件是否存在。
---
## 7. 常犯的錯
1. **用不存在的邊**`ON_FAILURE``ON_TRUE`)→ 只有 `ON_SUCCESS``對每個 X`
2. **第一個節點不是 `input`**
3. **把 recipe 當零件寫**——`telegram_send``gmail``kbdb_get`**recipe** 不是零件
→ 寫成 `http_request` 該 recipe
4. 🔴 **查詢回 `missing` 就改寫成 `code` 節點**
→ 那叫「腹語術」(表面用 Arcrun、實際全寫 JS)。正解:缺 API 寫 recipe、缺能力投稿零件。
`code` 只用在**局部整形**(例:剝掉 LLM 回應的雜訊),不用來取代零件與流程控制。
---
## 8. 相關
- 完整版指引與十題考卷(含 haiku 實測 10/10):
頂層 repo `system-dev/docs/3-specs/arcrun-usable/`
- 下一步該讀哪支 skill`arcrun_list_skills()`
- 定期掃資料 → `build_watcher_workflow`
- RAG 檢索問答 → `rag_with_arcrun`
- workflow 卡住不動 → `debug_paused_workflow`
+94 -3
View File
@@ -8,6 +8,52 @@
## 待裁決
### P1|host fn 二進位通道(解開「零件無法處理非文字檔」的框架級限制)— 2026-07-27
**觸發**leo 07-27 原話(arcrun-rag t73 收尾時)——
> 「**解析各種檔案的零件還是要開發的,要記下來**,雖然跟現在無關,
> 是因為**這次我用他本機來跑閃掉了**。」
**現況=框架級硬牆(2026-07-27 實測,非推論)**
Arcrun 的 host function 邊界是 **UTF-8 文字通道 64 KB 上限**
`outBuf` 64KB`res.text()`host fn 內 `TextDecoder`)。後果:
1. **零件拿不到二進位** ⇒ PDF/docx/圖片這類檔案,**做不出解析零件**。
2. 雲端 workflow 的 `fetch_raw` 走同一顆 `http_request` ⇒ 同樣拿不到。
3. 連純文字都受 64KB 限:15 頁 PDF 抽出約 40KB 尚可,**25 頁以上會爆**
=「轉好了卻進不去」的靜默失敗。
**arcrun-rag 目前怎麼過關的(要講清楚,免得被誤會已解決)**
把轉檔搬到 **daemon 本機**`collector/convert.go` 調度層+PDFium-via-wazero)——
**這是繞過(work around),不是修好**。leo 的原話「用他本機來跑閃掉了」講的就是這件事。
**為什麼仍要做(不擋當前工作,但別遺忘)**
- **能力分裂**:現在只有「裝了 daemon 的人」有轉檔能力;**純雲端用戶永遠沒有**。
- **地端/雲端兩版格式支援會分岔**,長期維護兩套。
- 64KB 上限是**既有**限制,`.md` 今天就會撞到,PDF 只是讓它更快浮現。
**變更範圍(提案,待 leo confirm 才動)**
① host fn 加二進位/base64 通道(或 ArrayBuffer 傳遞)
② 放大或分塊化 `outBuf`(串流/分頁讀取,避免單次 64KB 天花板)
③ 之上才談「各格式解析零件」
**影響分析**
- **不受影響**:現行 active SDD `portal-auth` 的所有任務(不同層)。
- **不擋**arcrun-rag t73 本機轉檔層照做(已 commit `74d18ff`)。
- **牽動**`http_request` 零件、code 零件的 host fn 契約 ⇒ 屬**全人共用框架**改動,
影響所有既有零件,**必須 leo 拍板**(跨專案結構決策)。
**現在不做的理由**:leo 明說「跟現在無關」+客戶在等 daemon;本條僅**立案留底**
避免「本機能跑了」被後人誤讀成「這個限制已解決」。
## 已裁決
### ✅ confirmed 2026-07-24CP2-F cypher 二~四刀拆分(leo 授權總管技術裁決)
> **裁決依據**leo 2026-07-24 原話「不影響現在 CP 的話就排下一個,這個你是需要判斷的,不能是我,完全技術問題」。
> **總管裁決**confirm。排程=A4 全通+t21/t24 合併重部之後(與在途不撞)。
> **四題**:①開工時點=同上 ②刀④驗證=**手寫**(瘦身工程不再引依賴;兩鏈手寫+測試)③新 worker 命名=**arcrun-api** ④CP2-F 記載更正已由總管代辦。
> 開工時照 D35 ④ 先開新 SDD 搬任務再寫 code。
### P-2026-07-24CP2-F 第二~四刀——執行引擎獨立成精簡 worker(cypher 瘦身收官)
> 提案人:總管交辦之 arcrun 偵察 subagent(唯讀分析,未動 code、未部署)。
@@ -224,9 +270,6 @@ Actions 因防 flag 鐵律被整個刪除後(commit `037cf9b`),**寫入端
2. **C2**`get_component_guide` / `publish_component` 要「預設不掛載」還是「留著但改描述」?(品味/方向)
3. **C3**:零件是否真的已拆到另一個 repo?(leo 記憶待證實)
4. **B3 語意搜尋**:四類描述灌進 Vectorize 會產生 embedding 呼叫成本,確認可行?(花錢)
## 已裁決
### P-2026-07-19artifact-sharing 增補「Appbundle)+實例譜系+訂閱更新+多源分享」
> 狀態:**confirmed 2026-07-19**leo「好的可走」)。四個拍板點總管採建議值(leo 可翻案):①排序=Phase 1.5 緊接 Phase 1、在 KV 退休(#16/#17)前後皆可並行 ②訂閱預設 policy=notify ③側載標記第一波=列表標記即可 ④P6 第一波=官方源+直連一個外源,org 私區留第二波。tasks 增補見 artifact-sharing/tasks.md「Phase 1.5」段。
@@ -329,4 +372,52 @@ workflow/recipe/template 的實體在平台(CF/KBDB),不在 YAML——YAML
3. 側載標記的呈現強度:只列表標記,或 console 加「無譜系 workflow」提醒區塊?
4. P6 多源第一波範圍:只做「官方源+直連一個外源」?org 私區(visibility=org)是否留第二波?
---
## [proposal] 兩個真缺口要進 SDD2026-07-30,總管提,等 leo confirm
**來源**:頂層 CP `critical-paths/arcrun-usable.md`leo confirm 開工)逐步查證後,
發現兩件事**SDD 完全沒有**,而它們是「AI 不必寫 code」的前提。
leo 的判準(2026-07-30):
> 「如果在這裡編任務,不就是廢掉了原本的 SDD,那到底要照 SDD 做事還是照 CP?」
**照 SDD 做事、照 CP 排順序**。CP 不得自帶任務 ⇒ 這兩件必須進 SDD 才能做。
### 缺口 ①:引擎缺條件邊(`ON_TRUE``ON_FALSE`)→ 建議進 `arcrun-core-mvp`
- **實測**`if_control` 的 output 是 `{result: boolean, branch: "true"|"false"}`
`cypher-executor/src` 全域 grep `ON_TRUE|ON_FALSE` = **0**(圖的邊只有 `ON_SUCCESS`
- **後果**AI 照規矩用 `if_control` 也只拿到布林值,**還是得寫 code 判斷該走哪裡**
⇒ 這是「為什麼 workflow 全變成 code」的根(8 個 code 節點含 if×61
- **佐證**`kbdb_upsert_block` 契約自述「解 arcrun workflow **缺 IF/branch 能力**的缺口
arcrun.md P1 #1)」;Gitea **Arcrun#5**「if_control 無法真擋分支,filter+FOREACH 才是真閘」
**2026-07-04** 就發現,至今未修
- **SDD 查證**`grep "ON_TRUE|ON_FALSE|條件邊|分支"` 於全部 SDD → 7 命中**全是別義**
(「部署分支」「git 分支」),**條件邊本身完全沒設計過**
- **影響面**:改圖 schema `graph-executor` 走邊邏輯=引擎核心,風險最高
⇒ 建議排在「查詢誠實」「節點替換」驗過之後
### 缺口 ②:recipe 缺 payload 與回應處理層 → 建議進 `recipe-system`
- **實測**recipe schema 只有 `{canonical_id, endpoint, method, auth_service}`
(線上 `GET /recipes/telegram_send` 實查)
- **後果**`telegram_send` 的 description 明寫「body 帶 chat_id+text」但**schema 存不住 body**
⇒ 任何要帶 body 的 API 都只能繞過 recipe、把 URL/header/body 整包寫進 workflow
⇒ Gemini 就是這樣寫死的(`ask_llm` 三層全塌進節點)
- **要補三層**
- `body_template`payload
- `response_map`(回應正規化:取值路徑/思考型模型旗標/淨化規則)
——現在這段是 workflow 裡 2786 字元的 code`finalize`
- `auth` 加第四型 `binding`(免金鑰;一次打開 `env.AI``VECTORIZE``BROWSER``QUEUE`
- **SDD 查證**`grep "body_template|response_map|payload"` → 17 命中**全是別的 payload**
- **不做的話**LLM 換源永遠要改 workflow(而非換 recipe),違反「LLM 可換源」原則
### 誤標修正(誠實記錄)
總管原本在 CP 標了 **5 個新缺口**,逐條查 SDD 後**只有上述 2 個是真的**:
- `success_rate` 回寫 → **`arcrun-core-mvp/design.md:499` 早就設計**
`success_rate = success_runs / total_runs * 100`、排序規則、UI 顯示),只是沒實作
- 8 個壞範例、意圖節點替換 → 相關設計散在既有 SDD,已改為引用
### ⏸ 等 leo confirm
- confirm 後依 D35:兩份 SDD 目前皆 `paused`,要動需先處理單一活性
(現行 active`workflow-discovery`
@@ -1,6 +1,8 @@
---
status: active
superseded_by: ""
status: closed
superseded_by: "workflow-discovery"
closed_at: "2026-07-30"
closed_reason: "26/26 任務全完成、0 未完成待搬;封測 portal 多人授權已上線"
---
# portal-auth — DesignRAG Portal 多人授權)
@@ -1,5 +1,5 @@
---
status: paused
status: active
superseded_by: ""
---