Compare commits

..

1 Commits

Author SHA1 Message Date
uncle6me-web 614fe44812 fix(kbdb): 藏書地圖加 entry_count——triplet_count=0 不再被誤讀成「沒有知識」(Arcrun#87 三次收尾)
leo21c 實測:kbdb_get_map() 9 庫裡 8 庫 triplet_count:0/top_entities:[](kb 以外全部),
一個全新 session 讀到這份地圖會合理但錯誤地判定「這些庫沒有知識」而放棄查詢——但
kbdb_search(mode=keyword) 找得到 42 筆真內容(來源 gitea:Leo/Arcrun@…/gitea:Leo/mira@…等),
其中 15 筆 entries 的 metadata_json.library 已正確標成 arcrun/mira/arcrun-harness/arcrun-rag。

查證(唯讀,未動 leo21c 任何寫入):
- entries(原始 ingest 內容)與 triplet(從 entries 萃取出的三元組)是兩個不同存放處。
- kb 以外 7 庫:entries 有、library 標記正確;triplet 一筆都沒有——不是「三元組沒貼標」,
  是「三元組從沒被萃取」(另一條偵察線在查斷在哪一段,跨 repo,本 PR 不處理)。
- general 庫(未標 library 的三元組兜底分類)即時計數也是 0 ⇒ 沒有任何「有三元組但沒標庫」
  的候選 ⇒ Leo/Arcrun#87 先前那條批次補標通道(PR #114)在目前資料現況下會補到 0 筆,
  它解的是另一個問題,不是這次「地圖看起來是空的」的真因。

本次修法(純讀端加欄位,不碰任何寫入/部署/線上資源):
- kbdb/src/actions/library-map.ts:新增 liveEntryCountsByLibrary(),依 entries 自己的
  metadata_json.$.library 分組即時計數(排除 entry_type='value' 儲存碎片與地圖自己的歷史
  摘要 block,避免自我膨脹)。listLibraryMaps/getLibraryMapDetail/recomputeLibraryMap
  的回傳都加上 entry_count,與 triplet_count 並排、互不覆蓋。
- mcp/src/lib/library-map.ts:renderLibraryMapLines(MCP 連線開場注入的那份地圖原文)
  triplet_count=0 但 entry_count>0 時改印「0 triplets/N 筆原始內容(尚未萃取關係,
  kbdb_search 查得到)」,不再只印「0 triplets」。
- mcp/src/tools/kbdb_map.ts:kbdb_get_map 工具的全館/單庫回應都帶 entry_count,並在符合
  條件時附加提示,明講「triplet_count=0 不代表沒有知識」。
- console-ui/public/console/index.html:藏書地圖看板卡片同步顯示,人類看的畫面同一件事。

順手修掉一個真 SQL bug:entry_count 排除條件原寫
`NOT (entry_type='block' AND json_extract(...)='library_map')`,SQL 三值邏輯下
metadata_json 沒有 $.kind 欄位時 json_extract 回 NULL、`NULL = 'library_map'` 為 NULL
(非 false),整條 WHERE 判定 NULL 而把所有列濾掉——改用 COALESCE(...,'') 修正
(新增測試以 sqlite 實跑驗證抓到並鎖住這個修法)。

測試:kbdb 21/21(新增 2 案,全套 215/215);mcp kbdb-map 33/33(新增 7 案,全套 120/120)。
cypher-executor 既有 map/portal-data 相關測試(library-map-scope-108/kbdb-map-proxy/
portal-data)103/104 綠,唯一失敗(/portal HTML 殼 404)在未動過的 main checkout 上同樣
失敗,環境既有問題、與本次改動無關。

CP:◐ 半通。程式碼在此分支,測試綠燈,未併 main、未部署 leo21c——entry_count 要讓
leo21c 的真實使用者看到,需部署 kbdb+mcp(cypher-executor 未改動,portal-data.ts
是透明轉發不需重部署)。三元組萃取斷在哪一段(真因)與 23/42 entries 未 embed
(語意搜尋覆蓋率)兩件不在本 PR 範圍,分別交給另一條偵察線與 embed reconcile 管線。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 12:36:03 +08:00
18 changed files with 304 additions and 1196 deletions
Binary file not shown.
-14
View File
@@ -1,14 +0,0 @@
{
"name": "arcrun-component-worker-template",
"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"
}
}
-917
View File
@@ -1,917 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
hono:
specifier: ^4.7.0
version: 4.13.1
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20250408.0
version: 4.20260702.1
typescript:
specifier: ^5.4.0
version: 5.9.3
wrangler:
specifier: ^4.0.0
version: 4.121.0(@cloudflare/workers-types@4.20260702.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.20260804.1':
resolution: {integrity: sha512-191/PPEFicRK2wK69eXzSjnLgHL79k7zR2VUpyIr8rhFgGns1b5bTHgnBuUrgU2LPbEtwbE5eL2hJ0uhLAFEow==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@cloudflare/workerd-darwin-arm64@1.20260804.1':
resolution: {integrity: sha512-aI2cAFLsrNkSz3kLSQrgrO9ICTfY7jb2h0jgaWDE9mQLQQDfpXeYrKWt07tTgMmeNP79o9w3NZe3aqtt8mTGHQ==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@cloudflare/workerd-linux-64@1.20260804.1':
resolution: {integrity: sha512-KBCjxBIlN2jucfQGaTK4EgmPsWzQgYR/zYhPfi3mkWwdoTyG1dgrt2aizKps/SYse85ci/SOxojKk0/K7vstPw==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@cloudflare/workerd-linux-arm64@1.20260804.1':
resolution: {integrity: sha512-7lswfarBZ7xkHpTFrNb74ExwOltIalVaASc+GOYfCNtnwFxK/JZSVByfm/hnZEBtrHU7fMY2z7dYT64rwS0pHg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@cloudflare/workerd-windows-64@1.20260804.1':
resolution: {integrity: sha512-GOgRWYtxISN5rAWfx3K7zubt3xEn0/ZPFrbcLL+GwT4ouCKyNoHqfT1cPNB6Nx7t8BHEjnuTG7gkHO4Wd5ORdg==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20260702.1':
resolution: {integrity: sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@emnapi/runtime@1.11.3':
resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
'@esbuild/aix-ppc64@0.28.1':
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.28.1':
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.28.1':
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.28.1':
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.28.1':
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.28.1':
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.28.1':
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.28.1':
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.28.1':
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.28.1':
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.28.1':
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.28.1':
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.28.1':
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.28.1':
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.28.1':
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.28.1':
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.28.1':
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.28.1':
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.28.1':
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.28.1':
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.28.1':
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.28.1':
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.28.1':
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.28.1':
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.28.1':
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.28.1':
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
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.35.2':
resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.35.2':
resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
'@img/sharp-freebsd-wasm32@0.35.2':
resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
'@img/sharp-libvips-darwin-arm64@1.3.1':
resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.3.1':
resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.3.1':
resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-arm@1.3.1':
resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.3.1':
resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.3.1':
resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.3.1':
resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linux-x64@1.3.1':
resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.3.1':
resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.3.1':
resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-linux-arm64@0.35.2':
resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-arm@0.35.2':
resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@img/sharp-linux-ppc64@0.35.2':
resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-riscv64@0.35.2':
resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@img/sharp-linux-s390x@0.35.2':
resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@img/sharp-linux-x64@0.35.2':
resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.35.2':
resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@img/sharp-linuxmusl-x64@0.35.2':
resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@img/sharp-wasm32@0.35.2':
resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==}
engines: {node: '>=20.9.0'}
'@img/sharp-webcontainers-wasm32@0.35.2':
resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
'@img/sharp-win32-arm64@0.35.2':
resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-ia32@0.35.2':
resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.35.2':
resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==}
engines: {node: '>=20.9.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.24':
resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==}
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.28.1:
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
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.13.1:
resolution: {integrity: sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==}
engines: {node: '>=16.9.0'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
miniflare@5.20260804.1-alpha:
resolution: {integrity: sha512-J0QBHEj+d75TyFE9VhH+2dXgvdYt/pfyDlm+9IiYUFocQvqYy9iuW1DIqNawh1N7Kr6iMrDzVkgDSdt6pA75uA==}
engines: {node: '>=22.0.0'}
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.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
sharp@0.35.2:
resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==}
engines: {node: '>=20.9.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.29.0:
resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==}
engines: {node: '>=20.18.1'}
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
workerd@1.20260804.1:
resolution: {integrity: sha512-b0P38g5/ssemwWxd/mafNYggEZ0ere7PCiUH6RCHkgqjRhhXTP45nDiL2L3iIvi8uF2IjNrGpVgMXEunCaeY/w==}
engines: {node: '>=16'}
hasBin: true
wrangler@4.121.0:
resolution: {integrity: sha512-dcARWk6CyaD0vJBSLjJn4K2yo+mYko73hzryq+t/9DXnyAq877RWIMl7uOMcDKs5dDXdoickNhZTKxBYT9XKsA==}
engines: {node: '>=22.0.0'}
hasBin: true
peerDependencies:
'@cloudflare/workers-types': ^5.20260804.1
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
ws@8.21.0:
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
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.20260804.1)':
dependencies:
unenv: 2.0.0-rc.24
optionalDependencies:
workerd: 1.20260804.1
'@cloudflare/workerd-darwin-64@1.20260804.1':
optional: true
'@cloudflare/workerd-darwin-arm64@1.20260804.1':
optional: true
'@cloudflare/workerd-linux-64@1.20260804.1':
optional: true
'@cloudflare/workerd-linux-arm64@1.20260804.1':
optional: true
'@cloudflare/workerd-windows-64@1.20260804.1':
optional: true
'@cloudflare/workers-types@4.20260702.1': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@emnapi/runtime@1.11.3':
dependencies:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.28.1':
optional: true
'@esbuild/android-arm64@0.28.1':
optional: true
'@esbuild/android-arm@0.28.1':
optional: true
'@esbuild/android-x64@0.28.1':
optional: true
'@esbuild/darwin-arm64@0.28.1':
optional: true
'@esbuild/darwin-x64@0.28.1':
optional: true
'@esbuild/freebsd-arm64@0.28.1':
optional: true
'@esbuild/freebsd-x64@0.28.1':
optional: true
'@esbuild/linux-arm64@0.28.1':
optional: true
'@esbuild/linux-arm@0.28.1':
optional: true
'@esbuild/linux-ia32@0.28.1':
optional: true
'@esbuild/linux-loong64@0.28.1':
optional: true
'@esbuild/linux-mips64el@0.28.1':
optional: true
'@esbuild/linux-ppc64@0.28.1':
optional: true
'@esbuild/linux-riscv64@0.28.1':
optional: true
'@esbuild/linux-s390x@0.28.1':
optional: true
'@esbuild/linux-x64@0.28.1':
optional: true
'@esbuild/netbsd-arm64@0.28.1':
optional: true
'@esbuild/netbsd-x64@0.28.1':
optional: true
'@esbuild/openbsd-arm64@0.28.1':
optional: true
'@esbuild/openbsd-x64@0.28.1':
optional: true
'@esbuild/openharmony-arm64@0.28.1':
optional: true
'@esbuild/sunos-x64@0.28.1':
optional: true
'@esbuild/win32-arm64@0.28.1':
optional: true
'@esbuild/win32-ia32@0.28.1':
optional: true
'@esbuild/win32-x64@0.28.1':
optional: true
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.3.1
optional: true
'@img/sharp-darwin-x64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.3.1
optional: true
'@img/sharp-freebsd-wasm32@0.35.2':
dependencies:
'@img/sharp-wasm32': 0.35.2
optional: true
'@img/sharp-libvips-darwin-arm64@1.3.1':
optional: true
'@img/sharp-libvips-darwin-x64@1.3.1':
optional: true
'@img/sharp-libvips-linux-arm64@1.3.1':
optional: true
'@img/sharp-libvips-linux-arm@1.3.1':
optional: true
'@img/sharp-libvips-linux-ppc64@1.3.1':
optional: true
'@img/sharp-libvips-linux-riscv64@1.3.1':
optional: true
'@img/sharp-libvips-linux-s390x@1.3.1':
optional: true
'@img/sharp-libvips-linux-x64@1.3.1':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.3.1':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.3.1':
optional: true
'@img/sharp-linux-arm64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.3.1
optional: true
'@img/sharp-linux-arm@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.3.1
optional: true
'@img/sharp-linux-ppc64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-ppc64': 1.3.1
optional: true
'@img/sharp-linux-riscv64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-riscv64': 1.3.1
optional: true
'@img/sharp-linux-s390x@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.3.1
optional: true
'@img/sharp-linux-x64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.3.1
optional: true
'@img/sharp-linuxmusl-arm64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.3.1
optional: true
'@img/sharp-linuxmusl-x64@0.35.2':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.3.1
optional: true
'@img/sharp-wasm32@0.35.2':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
'@img/sharp-webcontainers-wasm32@0.35.2':
dependencies:
'@img/sharp-wasm32': 0.35.2
optional: true
'@img/sharp-win32-arm64@0.35.2':
optional: true
'@img/sharp-win32-ia32@0.35.2':
optional: true
'@img/sharp-win32-x64@0.35.2':
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.24': {}
blake3-wasm@2.1.5: {}
cookie@1.1.1: {}
detect-libc@2.1.2: {}
error-stack-parser-es@1.0.5: {}
esbuild@0.28.1:
optionalDependencies:
'@esbuild/aix-ppc64': 0.28.1
'@esbuild/android-arm': 0.28.1
'@esbuild/android-arm64': 0.28.1
'@esbuild/android-x64': 0.28.1
'@esbuild/darwin-arm64': 0.28.1
'@esbuild/darwin-x64': 0.28.1
'@esbuild/freebsd-arm64': 0.28.1
'@esbuild/freebsd-x64': 0.28.1
'@esbuild/linux-arm': 0.28.1
'@esbuild/linux-arm64': 0.28.1
'@esbuild/linux-ia32': 0.28.1
'@esbuild/linux-loong64': 0.28.1
'@esbuild/linux-mips64el': 0.28.1
'@esbuild/linux-ppc64': 0.28.1
'@esbuild/linux-riscv64': 0.28.1
'@esbuild/linux-s390x': 0.28.1
'@esbuild/linux-x64': 0.28.1
'@esbuild/netbsd-arm64': 0.28.1
'@esbuild/netbsd-x64': 0.28.1
'@esbuild/openbsd-arm64': 0.28.1
'@esbuild/openbsd-x64': 0.28.1
'@esbuild/openharmony-arm64': 0.28.1
'@esbuild/sunos-x64': 0.28.1
'@esbuild/win32-arm64': 0.28.1
'@esbuild/win32-ia32': 0.28.1
'@esbuild/win32-x64': 0.28.1
fsevents@2.3.3:
optional: true
hono@4.13.1: {}
kleur@4.1.5: {}
miniflare@5.20260804.1-alpha:
dependencies:
'@cspotcode/source-map-support': 0.8.1
sharp: 0.35.2
undici: 7.29.0
workerd: 1.20260804.1
ws: 8.21.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.5: {}
sharp@0.35.2:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
'@img/sharp-darwin-arm64': 0.35.2
'@img/sharp-darwin-x64': 0.35.2
'@img/sharp-freebsd-wasm32': 0.35.2
'@img/sharp-libvips-darwin-arm64': 1.3.1
'@img/sharp-libvips-darwin-x64': 1.3.1
'@img/sharp-libvips-linux-arm': 1.3.1
'@img/sharp-libvips-linux-arm64': 1.3.1
'@img/sharp-libvips-linux-ppc64': 1.3.1
'@img/sharp-libvips-linux-riscv64': 1.3.1
'@img/sharp-libvips-linux-s390x': 1.3.1
'@img/sharp-libvips-linux-x64': 1.3.1
'@img/sharp-libvips-linuxmusl-arm64': 1.3.1
'@img/sharp-libvips-linuxmusl-x64': 1.3.1
'@img/sharp-linux-arm': 0.35.2
'@img/sharp-linux-arm64': 0.35.2
'@img/sharp-linux-ppc64': 0.35.2
'@img/sharp-linux-riscv64': 0.35.2
'@img/sharp-linux-s390x': 0.35.2
'@img/sharp-linux-x64': 0.35.2
'@img/sharp-linuxmusl-arm64': 0.35.2
'@img/sharp-linuxmusl-x64': 0.35.2
'@img/sharp-webcontainers-wasm32': 0.35.2
'@img/sharp-win32-arm64': 0.35.2
'@img/sharp-win32-ia32': 0.35.2
'@img/sharp-win32-x64': 0.35.2
supports-color@10.2.2: {}
tslib@2.8.1:
optional: true
typescript@5.9.3: {}
undici@7.29.0: {}
unenv@2.0.0-rc.24:
dependencies:
pathe: 2.0.3
workerd@1.20260804.1:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20260804.1
'@cloudflare/workerd-darwin-arm64': 1.20260804.1
'@cloudflare/workerd-linux-64': 1.20260804.1
'@cloudflare/workerd-linux-arm64': 1.20260804.1
'@cloudflare/workerd-windows-64': 1.20260804.1
wrangler@4.121.0(@cloudflare/workers-types@4.20260702.1):
dependencies:
'@cloudflare/kv-asset-handler': 0.5.0
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260804.1)
blake3-wasm: 2.1.5
esbuild: 0.28.1
miniflare: 5.20260804.1-alpha
path-to-regexp: 6.3.0
unenv: 2.0.0-rc.24
workerd: 1.20260804.1
optionalDependencies:
'@cloudflare/workers-types': 4.20260702.1
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.21.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.24
cookie: 1.1.1
youch-core: 0.3.3
-142
View File
@@ -1,142 +0,0 @@
/**
* arcrun logic component Worker
*
* POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output
*
* WASM is statically bundled at build time via wrangler.toml [[wasm_modules]].
* Each logic component gets its own Worker at {name}.arcrun.dev.
*/
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
import { Hono } from 'hono';
import { cors } from 'hono/cors';
const app = new Hono();
app.use('*', cors());
app.get('/', (c) => c.json({ ok: true, component: COMPONENT_ID }));
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(componentWasm, input);
return c.json(result);
} catch (e) {
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500);
}
});
export default app;
// ── WASM runner (WASI preview1 stdin/stdout) ─────────────────────────────────
declare const COMPONENT_ID: string; // injected via [vars] in wrangler.toml
async function runWasm(wasmModule: WebAssembly.Module, input: unknown): Promise<unknown> {
const stdinBytes = new TextEncoder().encode(JSON.stringify(input));
let stdinOffset = 0;
const stdoutChunks: Uint8Array[] = [];
let memory: WebAssembly.Memory | null = null;
const getView = () => new DataView(memory!.buffer);
const wasi: WebAssembly.Imports = {
wasi_snapshot_preview1: {
fd_write(fd: number, iovs: number, iovs_len: number, nwritten_ptr: number): number {
if (fd !== 1 && fd !== 2) return 76; // ENOSYS
const view = getView();
let total = 0;
for (let i = 0; i < iovs_len; i++) {
const base = view.getUint32(iovs + i * 8, true);
const len = view.getUint32(iovs + i * 8 + 4, true);
if (len === 0) continue;
const chunk = new Uint8Array(memory!.buffer, base, len);
const copy = new Uint8Array(len);
copy.set(chunk);
if (fd === 1) stdoutChunks.push(copy);
total += len;
}
view.setUint32(nwritten_ptr, total, true);
return 0;
},
fd_read(fd: number, iovs: number, iovs_len: number, nread_ptr: number): number {
if (fd !== 0) return 76;
const view = getView();
let total = 0;
for (let i = 0; i < iovs_len; i++) {
const base = view.getUint32(iovs + i * 8, true);
const len = view.getUint32(iovs + i * 8 + 4, true);
const remaining = stdinBytes.length - stdinOffset;
if (remaining <= 0) break;
const toCopy = Math.min(len, remaining);
new Uint8Array(memory!.buffer, base, toCopy).set(
stdinBytes.subarray(stdinOffset, stdinOffset + toCopy)
);
stdinOffset += toCopy;
total += toCopy;
}
view.setUint32(nread_ptr, total, true);
return 0;
},
proc_exit(code: number): never { throw new Error(`wasm exit: ${code}`); },
random_get(ptr: number, len: number): number {
crypto.getRandomValues(new Uint8Array(memory!.buffer, ptr, len));
return 0;
},
fd_seek: () => 76, fd_close: () => 0,
fd_fdstat_get: () => 76, fd_prestat_get: () => 76,
fd_prestat_dir_name: () => 76, environ_get: () => 0,
environ_sizes_get: (cp: number, sp: number) => {
if (memory) { const v = getView(); v.setUint32(cp,0,true); v.setUint32(sp,0,true); }
return 0;
},
args_get: () => 0,
args_sizes_get: (ap: number, bp: number) => {
if (memory) { const v = getView(); v.setUint32(ap,0,true); v.setUint32(bp,0,true); }
return 0;
},
clock_time_get: (_id: number, _prec: bigint, tp: number) => {
if (memory) getView().setBigUint64(tp, BigInt(Date.now()) * 1_000_000n, true);
return 0;
},
clock_res_get: () => 76, poll_oneoff: () => 76, sched_yield: () => 0,
proc_raise: () => 76, sock_accept: () => 76, sock_recv: () => 76,
sock_send: () => 76, sock_shutdown: () => 76,
path_open: () => 76, path_create_directory: () => 76,
path_remove_directory: () => 76, path_rename: () => 76,
path_unlink_file: () => 76, path_filestat_get: () => 76,
path_readlink: () => 76, path_symlink: () => 76, path_link: () => 76,
},
// u6u host functions (no-op for pure logic components)
u6u: { http_request: () => 1 },
};
const instance = await WebAssembly.instantiate(wasmModule, wasi);
memory = instance.exports.memory as WebAssembly.Memory;
const start = (instance.exports._start ?? instance.exports.main) as () => void;
if (typeof start !== 'function') throw new Error('WASM missing _start or main export');
try { start(); } catch (e) {
if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e;
}
const decoder = new TextDecoder();
const total = stdoutChunks.reduce((n, c) => n + c.length, 0);
const merged = new Uint8Array(total);
let off = 0;
for (const chunk of stdoutChunks) { merged.set(chunk, off); off += chunk.length; }
const stdout = decoder.decode(merged).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
}
}
+6 -1
View File
@@ -840,7 +840,12 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
'<div class="kt">' + esc(l.library) + (S.library === l.library ? ' <span class="tag" style="font-size:11.5px;vertical-align:middle">搜尋中</span>' : '') + '</div>' +
'<div class="ks">' + (l.narrative ? esc(l.narrative) : '<span class="dim">(此庫尚無 narrative——recompute 時可帶入)</span>') + '</div>' +
(ents && ents.length ? '<div style="display:flex;gap:6px;flex-wrap:wrap">' + lmEntityLine(ents) + '</div>' : '') +
'<div class="km"><span class="mono" style="color:var(--amber)">' + (Number(l.triplet_count) || 0) + ' 三元組</span>' +
// Arcrun#87 三次收尾(2026-08-13):triplet_count=0 不等於這庫沒有知識——entries 有標庫、
// 只是三元組萃取沒對它跑過(kb 以外幾乎全庫皆此況)。entry_count>0 時附一句,別讓看板
// 的「0 三元組」被讀成「這庫是空的」(同 MCP 端 kbdb_map.ts 的 entryOnlyHint 同一件事)。
'<div class="km"><span class="mono" style="color:var(--amber)">' + (Number(l.triplet_count) || 0) + ' 三元組' +
(!(Number(l.triplet_count) || 0) && (Number(l.entry_count) || 0) > 0
? '<span class="dim">・' + (Number(l.entry_count) || 0) + ' 筆原始內容(尚未萃取)</span>' : '') + '</span>' +
'<span class="dim" style="margin-left:auto;font-size:12.5px">' + stamp + '</span></div></div>';
}).join('');
}
@@ -51,11 +51,6 @@ const WASM_HTTP_RUNNER_IDS: ReadonlySet<string> = new Set([
'auth_service_account',
'auth_oauth2',
'auth_mtls',
// hashArcrun#912026-08-13):純計算零件(sha256/sha1/md5hex/base64),
// 出貨線版本號機制與成品指紋核對用它。no_network_syscall,故不走 LOGIC_BINDING_MAP
// 的 Service Binding 路(rule 3.1 禁新增 binding),走這裡的通用 wasmWorkerUrl 推導,
// 與 code/cron 同一形狀(獨立 Worker,白名單只是「知道這個 canonical_id 存在」)。
'hash',
]);
/**
@@ -1,59 +0,0 @@
/**
* component-loader`hash` Arcrun#91
*
* workflow `component: hash` component-loader step 1-6
* builtin / URL / hash-id / LOGIC_BINDING_MAP / recipe
* step 7 WASM_HTTP_RUNNER_IDS wasmWorkerUrl
* `hash` step 8 #29 `code`
*
*
* 1. `hash` runner throw
* 2. runner URL = arcrun-hash.{WORKER_SUBDOMAIN}.workers.dev
* hash.arcrun.dev self-hosted WORKER_SUBDOMAIN
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { env } from 'cloudflare:test';
import { createComponentLoader, wasmWorkerUrl } from '../src/lib/component-loader';
import type { Bindings } from '../src/types';
afterEach(() => {
vi.unstubAllGlobals();
});
describe('component-loaderhash 零件解析(WASM HTTP runner 白名單,Arcrun#91', () => {
it('wasmWorkerUrl 通用推導:hash → arcrun-hash.{subdomain}.workers.devself-hosted 換 subdomain 即成立)', () => {
expect(wasmWorkerUrl('hash', 'uncle6-me')).toBe('https://arcrun-hash.uncle6-me.workers.dev');
expect(wasmWorkerUrl('hash', 'my-selfhosted-sub')).toBe('https://arcrun-hash.my-selfhosted-sub.workers.dev');
});
it('component: hash 能被解析成 runner,且 fetch 打 subdomain 推導的 URL', async () => {
const fakeEnv = {
...env,
WORKER_SUBDOMAIN: 'test-sub',
} as unknown as Bindings;
const loader = createComponentLoader(fakeEnv);
// 白名單漏 `hash` 時這裡就 throw「找不到零件 "hash"」
const runner = await loader('hash');
expect(typeof runner).toBe('function');
// stub fetch 捕 URL:證明 runner 打的是通用推導 URL,非寫死官方域名
const calledUrls: string[] = [];
const fetchSpy = vi.fn(async (url: unknown) => {
calledUrls.push(String(url));
return new Response(
JSON.stringify({
success: true,
data: { result: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', algorithm: 'sha256', encoding: 'hex' },
}),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
);
});
vi.stubGlobal('fetch', fetchSpy);
const result = await runner({ algorithm: 'sha256', input: 'hello', encoding: 'hex' }) as Record<string, unknown>;
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(calledUrls[0]).toBe('https://arcrun-hash.test-sub.workers.dev');
expect(result.success).toBe(true);
});
});
+65 -11
View File
@@ -38,6 +38,13 @@ export interface LibraryMapRow {
narrative: string | null;
top_entities: string[]; // 全館視圖只回 top 3 名字(數百 token 內,design §4 MCP instructions 用)
triplet_count: number;
// Arcrun#87 三次收尾(2026-08-13,「藏書地圖說實話」):triplet_count=0 不等於「這庫沒有知識」——
// entriesingest 進來的原始卡片/skill 內容)與 triplet(從 entries 萃取出的三元組)是兩件事,
// 一個庫可能有大量 entries、但三元組萃取從沒對它跑過(實測:kb 以外 7 庫皆此況,entries 存在
// 且 library 標記正確,triplet 一筆都沒有)。entry_count 讓讀端(AIMCP 渲染層)分得清
// 「真的沒東西」與「有東西但還沒被萃取成三元組」,不再把後者誤讀成前者、誤判庫是空的而放棄查詢
// (見本檔 liveEntryCountsByLibrary 的計數口徑)。
entry_count: number;
updated_at: number;
}
@@ -50,6 +57,7 @@ export interface LibraryMapDetail {
relation_profile: RelationStat[];
bridges: Bridge[];
triplet_count: number;
entry_count: number; // 同 LibraryMapRow.entry_count(見該欄位註解)
commit_hash: string | null;
status: string;
updated_at: number;
@@ -285,6 +293,10 @@ export async function recomputeLibraryMap(db: D1Database, input: RecomputeInput)
superseded.push(row.rid);
}
// entry_countArcrun#87 三次收尾):recompute 只重算三元組那一半,entry_count 是即時算的
// 另一半,兩者同樣的道理一起回傳(見 LibraryMapDetail.entry_count 欄位註解)。
const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0;
return {
map: {
record_id: blockEntry.id,
@@ -295,6 +307,7 @@ export async function recomputeLibraryMap(db: D1Database, input: RecomputeInput)
relation_profile: relationProfile,
bridges,
triplet_count: tripletCount,
entry_count: entryCount,
commit_hash: input.commit_hash ?? null,
status: 'active',
updated_at: blockEntry.created_at,
@@ -367,6 +380,34 @@ async function liveTripletCountsByLibrary(
return m;
}
// 這個 owner 底下、依 entries 自己的 metadata_json.$.library 分組的即時「原始內容」數
//Arcrun#87 三次收尾,2026-08-13)——與 liveTripletCountsByLibrary 算的是兩張完全不同的帳:
// 那個算「萃取出幾條三元組」,這個算「ingest 進來幾筆原始卡片/skill/block」。兩者天生會不一樣
// (萃取是下游、有延遲甚至從沒對某些庫跑過),刻意分開算、分開回傳,不是同一數字的兩種寫法。
//
// 排除口徑(都是結構性排除,不是內容語意判斷——沒有違反「base 對內容語意無知」):
// - entry_type='value'record-crud.ts 的通用機制,任何 template(含 triplet 本身)建 record
// 時每個 slot 值都會落一筆這種 entry,是儲存實作細節,不是使用者看得到的「一筆知識」。
// - metadata.kind='library_map' 的 block:地圖自己每次 recompute 產出的摘要 block(含歷史
// superseded 的),算進去會自我膨脹、且無限接近雞生蛋——地圖不該把自己算進地圖裡。
async function liveEntryCountsByLibrary(db: D1Database, owner_id?: string): Promise<LibraryCountMap> {
const params: unknown[] = owner_id ? [owner_id] : [];
const res = await db
.prepare( // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆)
`SELECT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library,
COUNT(*) AS n
FROM entries
WHERE ${owner_id ? 'owner_id = ? AND ' : ''}entry_type != 'value'
AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map')
GROUP BY COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general')`,
)
.bind(...params)
.all<{ library: string; n: number }>();
const m: LibraryCountMap = new Map();
for (const r of res.results ?? []) m.set(r.library, r.n);
return m;
}
// 「已知庫名」集合:即使目前三元組數是 0,只要蓋過章(entries metadata.libraryt52 慣例)或
// 登記過(portal_library record),就不算「查無此庫」——用來分辨 GET /map/:library 的
// 「這庫是空的」(回 200triplet_count:0vs「查無此庫」(回 404)。kbdb base 對 portal_library
@@ -453,20 +494,26 @@ interface MapPivotRow {
ts: number;
}
// 全館地圖:每庫一行(librarynarrativetop 3 entitiestriplet_count),MCP instructions
// 直接嵌用(設計上限=數百 token,R3)。template 還不存在(從未 recompute)→ 誠實回空清單。
// 全館地圖:每庫一行(librarynarrativetop 3 entitiestriplet_countentry_count),MCP
// instructions 直接嵌用(設計上限=數百 token,R3)。template 還不存在(從未 recompute)→
// 誠實回空清單。entry_count 與 triplet_count 並排回傳(Arcrun#87 三次收尾):一個查「原始內容
// 有沒有」、一個查「萃取出幾條三元組」,兩者不同源、允許不一致(見 liveEntryCountsByLibrary
// 註解),讀端不該把其中一個的 0 當成另一個的答案。
export async function listLibraryMaps(db: D1Database, owner_id?: string): Promise<LibraryMapRow[]> {
const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME);
if (!tpl) return [];
const params: unknown[] = owner_id ? [tpl.id, owner_id] : [tpl.id];
const res = await db
.prepare(
`WITH m AS (${mapPivotSql(!!owner_id)})
SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL
ORDER BY m.ts DESC`,
)
.bind(...params)
.all<MapPivotRow>();
const [res, entryCounts] = await Promise.all([
db
.prepare( // kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解
`WITH m AS (${mapPivotSql(!!owner_id)})
SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL
ORDER BY m.ts DESC`,
)
.bind(...params)
.all<MapPivotRow>(),
liveEntryCountsByLibrary(db, owner_id),
]);
// 每庫只留最新 activesupersede 失敗殘留多個 active 時,讀端自癒取最新——順序安全的另一半)。
const byLib = new Map<string, LibraryMapRow>();
for (const r of res.results ?? []) {
@@ -476,6 +523,7 @@ export async function listLibraryMaps(db: D1Database, owner_id?: string): Promis
narrative: r.narrative || null,
top_entities: parseJsonArray<TopEntity>(r.top_entities).slice(0, 3).map((t) => t.name),
triplet_count: Number(r.triplet_count ?? 0) || 0,
entry_count: entryCounts.get(r.library) ?? 0,
updated_at: r.ts,
});
}
@@ -501,7 +549,12 @@ export async function getLibraryMapDetail(
.first<MapPivotRow>();
if (!row) return null;
// record_idmap block entry idrecompute 寫入時綁定);entry 若被外力刪除,content 誠實回 null。
const blockEntry = await getEntry(db, row.rid);
// entry_count 與全館視圖(listLibraryMaps)同一套計法(liveEntryCountsByLibrary),單庫詳圖
// 只取自己那一庫的數字——Arcrun#87 三次收尾,理由見 LibraryMapDetail.entry_count 欄位註解。
const [blockEntry, entryCounts] = await Promise.all([
getEntry(db, row.rid),
liveEntryCountsByLibrary(db, owner_id),
]);
return {
record_id: row.rid,
library,
@@ -511,6 +564,7 @@ export async function getLibraryMapDetail(
relation_profile: parseJsonArray<RelationStat>(row.relation_profile),
bridges: parseJsonArray<Bridge>(row.bridges),
triplet_count: Number(row.triplet_count ?? 0) || 0,
entry_count: entryCounts.get(library) ?? 0,
commit_hash: row.commit_hash || null,
status: row.status ?? 'active',
updated_at: row.ts,
+57
View File
@@ -361,6 +361,63 @@ describe('M3 收尾 — 即時新鮮度(ensureFreshLibraryMaps,讀端自動
expect(unknown.status).toBe(404); // 真的從沒出現過的名字才 404
});
it('entry_count 讓「有原始內容但三元組從沒萃取過」與「真的什麼都沒有」分得清(Arcrun#87 三次收尾)', async () => {
// 情境沿用上一案的 'hr' 庫(entries 蓋過章、triplet_count:0),這正是 leo21c 實測到的真實
// 現況(kb 以外 7 庫皆此況):地圖過去只回 triplet_count,AI 讀到 0 就誤判「這庫沒有知識」,
// 但實際上 kbdb_search 找得到內容——因為 entries 一直都在,只是沒被萃取成三元組。
const db = makeSqliteD1();
await seedTripletTemplate(db);
await ensureTripletLibrarySlot(db, 'triplet');
await createEntry(db, {
content: '人資資料 A',
entry_type: 'block',
owner_id: 'leo',
metadata_json: JSON.stringify({ library: 'hr' }),
});
await createEntry(db, {
content: '人資資料 B',
entry_type: 'block',
owner_id: 'leo',
metadata_json: JSON.stringify({ library: 'hr' }),
});
const { app, env } = makeApp(db);
// 單庫詳圖:triplet_count 仍是 0(沒騙這件事),但 entry_count 誠實回 2。
const detailRes = await app.request('/map/hr?owner_id=leo', {}, env);
const detailBody = (await detailRes.json()) as { map: { triplet_count: number; entry_count: number } };
expect(detailBody.map.triplet_count).toBe(0);
expect(detailBody.map.entry_count).toBe(2);
// 全館視圖:同一個庫、同一組數字,兩個 consumerGET /map、GET /map/:library)不能對不上。
const listRes = await app.request('/map?owner_id=leo', {}, env);
const listBody = (await listRes.json()) as { libraries: { library: string; triplet_count: number; entry_count: number }[] };
const hr = listBody.libraries.find((l) => l.library === 'hr');
expect(hr).toBeDefined();
expect(hr!.triplet_count).toBe(0);
expect(hr!.entry_count).toBe(2);
});
it('entry_count 的計數口徑排除 value 型 entries 與地圖自己的歷史摘要 block(不自我膨脹)', async () => {
// record-crud 建 triplet record 時,每個 slot 值都會落一筆 entry_type='value' 的 entry
// (儲存實作細節,不是「一筆知識」);地圖 recompute 也會建 entry_type='block' 且
// metadata.kind='library_map' 的摘要 entry——兩者都不該被算進 entry_count,否則地圖會把
// 自己的內部管線雜訊當成「使用者知識」回報,數字沒有意義。
const db = makeSqliteD1();
await seedTripletTemplate(db);
await ensureTripletLibrarySlot(db, 'triplet');
// 建一條 kb 三元組 → 連帶產生數筆 entry_type='value' 的 entries(不該被算進 entry_count)。
await seedTriplet(db, { s: 'A', p: '連結至', o: 'B', library: 'kb' });
// 觸發一次 recompute → 產生一筆 entry_type='block'/metadata.kind='library_map' 的摘要 entry
// (同樣不該被算進 entry_count)。
await recomputeLibraryMap(db, { library: 'kb' });
const detail = await getLibraryMapDetail(db, 'kb');
// 這個情境下 kb 沒有任何「真的 ingest 進來的原始內容」entry(只有 value 碎片與地圖自己的摘要),
// entry_count 應誠實回 0——不能因為底層 entries 表其實有好幾筆就報出一個誤導的非零數字。
expect(detail!.entry_count).toBe(0);
expect(detail!.triplet_count).toBe(1); // 對照:triplet_count 不受這個排除規則影響,維持原樣。
});
it('owner 隔離:即時新鮮度層不會把別的 owner 的三元組算進來', async () => {
const db = makeSqliteD1();
await seedTripletTemplate(db);
+22 -3
View File
@@ -27,6 +27,10 @@ export interface LibraryMapRow {
narrative: string | null;
top_entities: unknown; // 正常是 string[];防禦:舊部署可能回 JSON 字串形
triplet_count: number | string;
// Arcrun#87 三次收尾(2026-08-13,「藏書地圖說實話」):triplet_count=0 不等於這庫沒有知識——
// 可能只是三元組萃取從沒對它跑過,但 entries(原始 ingest 內容)還在,kbdb_search 找得到。
// 選填是因為舊部署(未帶這次修法)的 kbdb base 回應裡不會有這個欄位,容錯當 0。
entry_count?: number | string;
updated_at?: number;
}
@@ -65,7 +69,15 @@ export function entityNames(raw: unknown, limit: number): string[] {
const MAX_LIBRARY_LINES = 30;
const MAX_NARRATIVE_CHARS = 60;
/** 把 GET /map 的 libraries[] 渲染成緊湊文字(每庫一行,design §4 指定格式)。空清單回 null。 */
/**
* GET /map libraries[] design §4 null
*
* Arcrun#87 2026-08-13 session MCP
* triplet_countkb 0 triplets
* session 7 entries ingest
* kbdb_search triplet_count0
* entry_count0
*/
export function renderLibraryMapLines(libraries: LibraryMapRow[]): string | null {
const rows = libraries.filter((l) => l && typeof l.library === "string" && l.library);
if (rows.length === 0) return null;
@@ -74,8 +86,15 @@ export function renderLibraryMapLines(libraries: LibraryMapRow[]): string | null
const clipped =
narrative.length > MAX_NARRATIVE_CHARS ? `${narrative.slice(0, MAX_NARRATIVE_CHARS)}` : narrative;
const core = entityNames(l.top_entities, 3);
const count = Number(l.triplet_count ?? 0) || 0;
return `- ${l.library}${clipped}|核心:${core.length ? core.join("、") : "(尚無)"}${count} triplets`;
const tripletCount = Number(l.triplet_count ?? 0) || 0;
const entryCount = Number(l.entry_count ?? 0) || 0;
const countPart =
tripletCount > 0
? `${tripletCount} triplets`
: entryCount > 0
? `0 triplets${entryCount} 筆原始內容(尚未萃取關係,kbdb_search 查得到)`
: `0 triplets0 內容`;
return `- ${l.library}${clipped}|核心:${core.length ? core.join("、") : "(尚無)"}${countPart}`;
});
const omitted = rows.length > MAX_LIBRARY_LINES ? `\n(其餘 ${rows.length - MAX_LIBRARY_LINES} 庫略,kbdb_get_map 可看全部)` : "";
return lines.join("\n") + omitted;
+37 -3
View File
@@ -5,13 +5,20 @@
* MCP KBDB service bindingkbdbFetch GET /map/map/:library
* D1 binding #68 kbdb_graph_neighbors D17 KBDB MCP kbdb_*
*
* kbdb/src/routes/map.tsM2 merge
* kbdb/src/routes/map.tsM2 mergeentry_count Arcrun#87
* GET /map { success, libraries:[{library, narrative, top_entities( top3),
* triplet_count, updated_at}], count }
* triplet_count, entry_count, updated_at}], count }
* GET /map/:library { success, map:{record_id, library, narrative, content, top_entities,
* relation_profile, bridges, triplet_count, commit_hash, status, updated_at} }
* relation_profile, bridges, triplet_count, entry_count, commit_hash,
* status, updated_at} }
* 404 { success:false, error:'not found' } recompute
*
* entry_count vs triplet_count
* triplet_countentry_countingest /skill/block
* 0 0 triplet_count0
* entry_count0 kbdb_search triplet_count0
*
*
* slot top_entities/relation_profile/bridges JSON base parse
* live parse crash
*/
@@ -59,11 +66,23 @@ interface LibraryMapDetail {
relation_profile?: unknown;
bridges?: unknown;
triplet_count?: number | string;
// Arcrun#87 三次收尾:同 lib/library-map.ts LibraryMapRow.entry_count 的欄位(見該檔註解)。
entry_count?: number | string;
commit_hash?: string | null;
status?: string;
updated_at?: number;
}
/** triplet_count0 但 entry_count0 時給的提示(別讓「0 triplets」被讀成「這庫沒有知識」)。 */
function entryOnlyHint(tripletCount: number, entryCount: number, library?: string): string[] {
if (tripletCount > 0 || entryCount <= 0) return [];
const lib = library ? `${library}` : "這個庫";
return [
`${lib} triplet_count0,但有 ${entryCount} 筆原始內容(entries)——三元組萃取還沒對它跑過,` +
"不代表沒有知識。用 kbdb_search(關鍵字或語義)直接查得到內容。",
];
}
/**
* kbdb_get_map library
* design §6 retrieval get_map(library) graph/search
@@ -113,6 +132,7 @@ export function registerGetMap(server: McpServer, env: Env, identity: KnowledgeI
// 防禦:top_entities 若是 JSON 字串形就 parse 成名字清單(失敗當空,誠實不 crash)。
top_entities: entityNames(l.top_entities, 3),
triplet_count: Number(l.triplet_count ?? 0) || 0,
entry_count: Number(l.entry_count ?? 0) || 0,
}));
if (libraries.length === 0) {
// 空庫誠實回報:不是錯誤(端點正常)。地圖是讀時即時核對重算的(見 RECOMPUTE_HINTS
@@ -128,9 +148,21 @@ export function registerGetMap(server: McpServer, env: Env, identity: KnowledgeI
...RECOMPUTE_HINTS,
]);
}
// Arcrun#87 三次收尾:某些庫 triplet_count0 但 entry_count0entries 有、三元組
// 萃取沒跑過)——這件事只在「這種庫真的存在」時才提一次,不是每個庫都印一行(避免洗版),
// 讓讀這份地圖的 session 知道「觸目所及的 0」不能直接當成「沒有知識」。
const entryOnlyLibs = libraries.filter(
(l) => (l.triplet_count ?? 0) === 0 && (l.entry_count ?? 0) > 0,
);
return successResponse({ libraries, count: libraries.length }, [
"要看某庫細節:kbdb_get_map(library='庫名')",
"進庫查內容:kbdb_search(關鍵字/語義);查關係:kbdb_graph_neighbors",
...(entryOnlyLibs.length > 0
? [
`${entryOnlyLibs.map((l) => l.library).join("、")} 這幾庫 triplet_count0 但 entry_count0` +
"有原始內容,只是還沒萃取出三元組關係——別把 0 triplets 讀成「沒有知識」,直接 kbdb_search 進去查。",
]
: []),
]);
}
@@ -167,10 +199,12 @@ export function registerGetMap(server: McpServer, env: Env, identity: KnowledgeI
relation_profile: parseSlotArray<{ predicate: string; count: number }>(raw.relation_profile),
bridges: parseSlotArray<{ entity: string; libraries: string[] }>(raw.bridges),
triplet_count: Number(raw.triplet_count ?? 0) || 0,
entry_count: Number(raw.entry_count ?? 0) || 0,
};
return successResponse({ map }, [
"bridges=此庫 entity 同時出現在哪些其他庫(只有兩側三元組都標了 library 值才抓得到,舊資料若沒標會偏稀疏,是誠實現況不是 bug)",
"沿核心 entity 挖關係:kbdb_graph_neighbors(subject=entity 名)",
...entryOnlyHint(map.triplet_count, map.entry_count, library),
]);
} catch (e) {
return errorResponse("internal_error", e instanceof Error ? e.message : String(e), ["稍後重試"]);
+96
View File
@@ -121,6 +121,46 @@ describe("kbdb_get_map: 全館地圖(無參數)", () => {
expect(calls[0].url.searchParams.get("owner_id")).toBe("leo");
});
it("Arcrun#87 三次收尾:triplet_count=0 但 entry_count>0 的庫附上「別當成沒有知識」的提示", async () => {
// leo21c 實測現況:kb 以外幾乎每庫都長這樣——entries 有(且 library 標對),三元組萃取沒跑過。
const emptyTripletRow = {
library: "arcrun",
narrative: null,
top_entities: [],
triplet_count: 0,
entry_count: 15,
updated_at: 1786330534,
};
const { server, tools } = makeServer();
const { env } = makeEnv(
() => new Response(JSON.stringify({ success: true, libraries: [emptyTripletRow], count: 1 })),
);
registerGetMap(server, env, SERVICE);
const res = await tools.get("kbdb_get_map")!.handler({});
const body = parseResult(res);
const data = body.data as { libraries: { library: string; triplet_count: number; entry_count: number }[] };
expect(data.libraries[0].entry_count).toBe(15);
expect(data.libraries[0].triplet_count).toBe(0);
// 提示要點名該庫、講清楚「有內容只是沒萃取」,別讓 AI 讀到 0 triplets 就跳過這個庫。
const hints = (body.hints as string[]).join(" ");
expect(hints).toContain("arcrun");
expect(hints).toContain("entry_count");
expect(hints).toContain("kbdb_search");
});
it("entry_count 缺席(舊部署未帶此欄位)→ 容錯當 0,不 crash、不誤發提示", async () => {
const legacyRow = { ...KB_ROW }; // KB_ROW 本身沒有 entry_count 欄位(模擬舊部署回應)
const { server, tools } = makeServer();
const { env } = makeEnv(
() => new Response(JSON.stringify({ success: true, libraries: [legacyRow], count: 1 })),
);
registerGetMap(server, env, SERVICE);
const res = await tools.get("kbdb_get_map")!.handler({});
const body = parseResult(res);
const data = body.data as { libraries: { entry_count: number }[] };
expect(data.libraries[0].entry_count).toBe(0);
});
it("top_entities in JSON-string form is parsed defensively (live-observed slot shape)", async () => {
const { server, tools } = makeServer();
const row = {
@@ -213,6 +253,39 @@ describe("kbdb_get_map: 單庫詳圖(library 參數)", () => {
expect(map.bridges).toEqual([{ entity: "Gitea", libraries: ["notes"] }]);
});
it("Arcrun#87 三次收尾:單庫詳圖 triplet_count=0、entry_count>0 → 附「別當空庫」提示", async () => {
const emptyTripletDetail = {
...DETAIL,
library: "arcrun",
triplet_count: 0,
entry_count: 15,
top_entities: [],
relation_profile: [],
bridges: [],
};
const { server, tools } = makeServer();
const { env } = makeEnv(() => new Response(JSON.stringify({ success: true, map: emptyTripletDetail })));
registerGetMap(server, env, SERVICE);
const res = await tools.get("kbdb_get_map")!.handler({ library: "arcrun" });
const body = parseResult(res);
const map = (body.data as { map: { entry_count: number; triplet_count: number } }).map;
expect(map.triplet_count).toBe(0);
expect(map.entry_count).toBe(15);
const hints = (body.hints as string[]).join(" ");
expect(hints).toContain("arcrun");
expect(hints).toContain("kbdb_search");
});
it("triplet_count>0 的庫不附「別當空庫」提示(不需要時別洗版)", async () => {
const { server, tools } = makeServer();
const { env } = makeEnv(() => new Response(JSON.stringify({ success: true, map: DETAIL }))); // DETAIL.triplet_count=111
registerGetMap(server, env, SERVICE);
const res = await tools.get("kbdb_get_map")!.handler({ library: "kb" });
const body = parseResult(res);
const hints = (body.hints as string[]).join(" ");
expect(hints).not.toContain("entry_count");
});
it("JSON-string slot values are parsed into objects(任務規格); broken JSON → 空陣列", async () => {
const { server, tools } = makeServer();
const raw = {
@@ -343,6 +416,29 @@ describe("renderLibraryMapLines", () => {
it("empty input → null", () => {
expect(renderLibraryMapLines([])).toBeNull();
});
it("Arcrun#87 三次收尾:triplet_count=0 但 entry_count>0 → 開場那行不再讀成「這庫沒有知識」", () => {
// 這是 leo21c 的實際現況(kb 以外 7 庫皆此況)——這段渲染出的文字是全新 session 連上 MCP
// 第一眼看到的地圖,過去只印「0 triplets」,讀起來像空庫,session 因此跳過不查。
const text = renderLibraryMapLines([
{ library: "arcrun", narrative: null, top_entities: [], triplet_count: 0, entry_count: 15 },
]);
expect(text).toBe("- arcrun:(narrative 待補)|核心:(尚無)|0 triplets/15 筆原始內容(尚未萃取關係,kbdb_search 查得到)");
});
it("triplet_count=0 且 entry_count=0(真的沒有任何內容)→ 誠實講兩個都是 0", () => {
const text = renderLibraryMapLines([
{ library: "empty-lib", narrative: null, top_entities: [], triplet_count: 0, entry_count: 0 },
]);
expect(text).toBe("- empty-lib:(narrative 待補)|核心:(尚無)|0 triplets0 內容");
});
it("entry_count 缺席(舊部署未帶欄位)→ 容錯當 0,維持既有 triplet-only 措辭不 crash", () => {
const text = renderLibraryMapLines([
{ library: "kb", narrative: "摘要", top_entities: ["A"], triplet_count: 5 },
]);
expect(text).toBe("- kb:摘要|核心:A5 triplets");
});
});
// ── 2026-08-12:地圖也要跟著登入者的權限走 ────────────────────────────────────
+21 -27
View File
@@ -1,6 +1,6 @@
# 卡在人類閘前的產物(`Arcrun#89` / `#90`
# 卡在人類閘前的產物(`Arcrun#89` / `#90` / `#91`
> **為什麼這個資料夾存在**:這東西都做完並實測過了,但落地的最後一步是
> **為什麼這個資料夾存在**:這三樣東西都做完並實測過了,但落地的最後一步是
> **終端機裡等人親手打字的互動閘**AI 打不進去。
> 2026-08-11 它們原本只存在於某個 session 的暫存目錄——**那種目錄一關就沒了**。
> 先搶進版控,等人有空時再落地。
@@ -37,34 +37,28 @@ curl -s https://arcrun-cypher-executor.leo21c.workers.dev/auth-recipes/gitea
---
## 二、`hash` 零件(`#91`—— 已落地,不再放這裡
## 二、`hash` 零件(`#91`
> **2026-08-13 更新**:查證後,前一版寫的「互動閘」**不存在**——
> `scripts/component-arm.sh``.claude/hooks/component-guard.sh` 從未被建立過;
> `.claude/hooks/pre-write-guard.sh``registry/components/` 的限制只擋
> 「TS 檔」與「auth_* 命名放錯目錄」兩種情形(見該檔規則 1.1/1.2),
> 不擋新增像 `hash` 這樣的一般 TinyGo 零件目錄。**沒有那個只有人做得到的具體物件
> ⇒ 不是人閘,是可以直接走完的路。**
>
> `hash` 零件已正式走完零件投稿流程(`docs/component-pr-review-standard.md`
> checklist):
> - 原始碼在 `registry/components/hash/``main.go` / `component.contract.yaml` / `go.mod`
> - 部署包在 `.component-builds/hash/``component.wasm` 已 commit,符合 rule 05
> - `cypher-executor/src/lib/component-loader.ts``WASM_HTTP_RUNNER_IDS` 白名單已加
> `'hash'`(否則 `component: hash` 在任何實例上都會落到 step 8「找不到零件」,
> 與 `code` 零件當年 #29 的缺口同形狀),有對應單元測試
> `cypher-executor/tests/component-loader-hash.test.ts`
> - 已部署到 youlin 測試場(`arcrun-hash.youlin-hsieh-dev.workers.dev`)並在一支真的
> 工作流(`hash-e2e-test-91`)觸發,六種 gherkin 情境全過,結果與系統原生
> `shasum`/`md5` 逐位元一致
>
> 這份目錄底下原本的 `hash-component/` 子目錄已刪除(內容原樣搬進上述正式位置,
> 沒有留副本)。細節與驗證證據見對應 PR 說明。
`hash-component/` — sha256sha1md5hexbase64。出貨線的版本號機制與成品指紋核對都要它。
**已實測**tinygo 編出來、wasmtime 真跑,三種演算法都跟系統原生指令**逐位元一致**)。
`.wasm` 是 1.3 MB 編譯產物,**沒有進版控**——要驗自己重編:
```
cd pending-human-gate/hash-component && tinygo build -target=wasi -o /tmp/hash.wasm main.go
echo '{"algorithm":"sha256","input":"hello"}' | wasmtime /tmp/hash.wasm
printf 'hello' | shasum -a 256 # 兩者應該一致
```
**落地要走零件投稿流程**(D27/D28):`docs/component-pr-review-standard.md` 的 checklist
人在終端機互動跑 `scripts/component-arm.sh`
🔴 `registry/components/` 底下有機械閘(`component-guard.sh`)擋著 AI 直接寫入——**那是刻意的**,
所以這份放在 `pending-human-gate/`,不是放在它最終該去的位置。
---
## 落地之後
`#89` 仍待落地才能從 **◐ 半通** 變 **✅**——
判準是**貼一次真實的執行輸出**(recipe 對某個測試檔案回 2xx),不是「推上去了」。
`#91`hash 零件)已達成此判準,見上節
三樣都上去之後,`Arcrun#89``#91` 才能從 **◐ 半通** 變 **✅**——
判準是**貼一次真實的執行輸出**(recipe 對某個測試檔案回 2xx、零件在真端點上跑出正確雜湊),
不是「推上去了」
-3
View File
@@ -1,3 +0,0 @@
module component
go 1.21