diff --git a/contracts/ingest-candidate.json b/contracts/ingest-candidate.json index 0eb9d22..7c693a5 100644 --- a/contracts/ingest-candidate.json +++ b/contracts/ingest-candidate.json @@ -60,7 +60,7 @@ }, "nodes": { "type": "array", - "description": "節點層附帶資訊(選填)。entity_type 與 gloss 是【節點】屬性,不是【邊】屬性 → 放這裡,不放 triplets。graph 用 gloss 去 embed(每節點一句,不是裸詞)、用 entity_type 去 typing。", + "description": "節點層附帶資訊。【向量化分工(leo 2026-06-26,ingest#1 升格成契約)】ingest 在此【打標】哪些 token 要向量化 + embed 什麼;base/KBDB embed 模組【讀標執行】實際 embedding;ingest 自己不算向量。兩類節點(實體詞條 / wikilink 卡)都進 nodes[],謂詞向量見 triplets[].predicate_vector。", "items": { "type": "object", "required": ["name"], @@ -69,11 +69,26 @@ "name": { "type": "string", "minLength": 1, - "description": "節點名(須對應某 triplet 的 subject 或 object 原字面)。" + "description": "節點名(須對應某 triplet 的 subject/object 原字面)。實體詞條=正規名;wikilink 卡=卡標題。" + }, + "id": { + "type": "string", + "description": "去重鍵。wikilink 卡用【檔名】→ 一卡一 node,被多條邊指到也只 embed 一次,不以出現次數重複。實體詞條用正規名。選填(無則以 name 去重)。" }, "gloss": { "type": "string", - "description": "一句話描述,供 embedding。例如 'Graph RAG — 用關係遍歷檢索、保住異見的 RAG 變體'。選填(建議 deep tier 產出)。" + "description": "一句話描述。base embed 對【名 + gloss 一起】embedding(實體同義詞字面差太遠,靠描述拉近)。選填(建議 deep tier 產)。" + }, + "aliases": { + "type": "array", + "items": { "type": "string" }, + "description": "同義詞(如『黃仁勳』/『Jensen Huang』)。base 歸一(collapse)成同一 node。選填。" + }, + "embed": { + "type": "boolean", + "default": true, + "description": "【向量化打標】此節點要不要進向量庫。true=base 讀標去 embed(名+gloss);false=base 看到就不理(如結構符號/散文不該進 nodes[],真進了標 false)。預設 true(實體詞條與 wikilink 卡都要)。", + "$comment": "ingest 打標,base 讀標執行。embed 動作歸 base embed 模組,ingest 不算向量。" }, "entity_type": { "type": "string", @@ -86,7 +101,7 @@ "triplets": { "type": "array", "minItems": 1, - "description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence。", + "description": "邊(關係)。ingest 只產原始 (s,p,o) + confidence + 謂詞向量打標。端點(s/o)以字面 match nodes[].name。", "items": { "type": "object", "required": ["subject", "predicate", "object"], @@ -95,10 +110,16 @@ "subject": { "type": "string", "minLength": 1, "description": "主詞(實體名,須與 nodes[].name 對得上若有提供)" }, "predicate": { "type": "string", "minLength": 1, "description": "謂詞(關係)" }, "object": { "type": "string", "minLength": 1, "description": "受詞(目標實體或值)" }, + "predicate_embed": { + "type": "boolean", + "default": true, + "description": "【謂詞向量化打標】謂詞要不要 embed。base 讀標 → embed【謂詞裸詞,無描述】(謂詞同義詞字面本就近,如『參考』/『參照』,裸詞 embed 即自動聚類),存 edge 的 predicate_vector。為支援『關係過濾』查詢(查『參考』不漏『參照』)→ 預設 true。embed 動作歸 base,ingest 只打標。", + "$comment": "ingest 打標,base 讀標執行 embed。" + }, "confidence":{ "type": "number", "minimum": 0, "maximum": 1, "default": 1.0, "description": "萃取可信度。淺萃可附自評;graph 不據此過濾,只記錄。" } } } } }, - "$comment": "禁止欄位(graph 領域,ingest 絕不可送): id / clusters / bridge_score / created_at / updated_at / 以及 triplet 上的 subject_entity_type|object_entity_type(類型只走 nodes[])。送了即違反 ingest=純餵食器的邊界,graph 應拒收或忽略。" + "$comment": "禁止欄位(graph 領域,ingest 絕不可送): id(節點去重鍵的 id 例外,那是 ingest 提供的去重鍵非 record id) / clusters / bridge_score / created_at / updated_at / 以及 triplet 上的 subject_entity_type|object_entity_type(類型只走 nodes[])。【向量化分工】ingest 打標(embed/predicate_embed + 帶 gloss/aliases),base/KBDB embed 模組讀標執行 embedding,ingest 不算向量。結構符號(>>/←)與給人讀的散文(## 摘要)不進 envelope。" } diff --git a/docs/3-specs/ingest-pipeline/tasks.md b/docs/3-specs/ingest-pipeline/tasks.md index 52dbaac..b83c585 100644 --- a/docs/3-specs/ingest-pipeline/tasks.md +++ b/docs/3-specs/ingest-pipeline/tasks.md @@ -2,44 +2,47 @@ > 唯一進度來源。狀態:[ ] 未開始 [🔄] 進行中 [x] 完成 [⏸] 卡住 > 跨專案藍圖:InkStoneCo `docs/3-specs/mira-dissolve/`。 +> 實作分支:`claude/ingest-t1-t5-implementation`(vitest 28 passed / tsc clean / dry-run 乾淨)。 -## T0 repo 骨架(本輪) +## T0 repo 骨架 - [x] 0.1 建 public repo `uncle6me-web/kbdb-ingest-plugin` - [x] 0.2 CLAUDE.md(上游指針 + ingest 鐵律)+ README + .gitignore - [x] 0.3 `contracts/ingest-candidate.json`(從頂層 SDD 複製,凍結契約) -- [x] 0.4 SDD 三件式骨架 -- [ ] 0.5 package.json / tsconfig / wrangler.toml(參考 kbdb-graph-plugin) +- [x] 0.4 SDD 三件式骨架(`docs/3-specs/ingest-pipeline/`) +- [x] 0.5 package.json / tsconfig / wrangler.toml / vitest.config(參考 kbdb-graph-plugin:Hono + zod-openapi,無 D1/Vectorize/AI 綁定) -## T1 SourceAdapter(R1) +## T1 SourceAdapter(R1)— `src/lib/source-adapter.ts` -- [ ] 1.1 GitHub 拉 repo(runtime API/clone,非 Actions) -- [ ] 1.2 content-hash(per-file,source.uri = github:owner/repo@path) -- [ ] 1.3 被 KBDB MCP `refresh` 代轉觸發的接口 +- [x] 1.1 GitHub 拉 repo(runtime git/trees + contents API,非 Actions);GitHubFetcher 介面(測試走 mock) +- [x] 1.2 content-hash(per-file sha256;source.uri = github:owner/repo@path,makeSourceUri/parseSourceUri round-trip) +- [x] 1.3 被 graph `POST /graph/refresh` 代轉觸發的受理端:`POST /refresh`(`src/index.ts`,被動代轉、無排程) -## T2 採取(R2,路徑 A 優先) +## T2 採取(R2,路徑 A 優先)— `src/lib/harvest.ts` -- [ ] 2.1 拉本地 CC 已建三元組 + gloss(用了 system-dev-template 的 repo) -- [ ] 2.2 cherry-pick `polaris/mira/tools/_kbdb_client.py` → 改純餵食器(POST envelope,不寫 KBDB) +- [x] 2.1 採取本地 CC 已建三元組 + gloss(template 1.8.0+ 格式:frontmatter gloss、`## 實體`、`## 關聯` typed-edge;卡對卡 vs 內文端點分流) +- [x] 2.2 cherry-pick `_kbdb_client.py` → 改純餵食器 `src/lib/graph-client.ts`(POST envelope,**不寫 KBDB/base**) -## T3 extract(R3,路徑 B fallback) +## T3 extract(R3,路徑 B fallback)— `src/lib/extract.ts` -- [ ] 3.1 cherry-pick `wiki_synthesis.yaml` classify / 兩 skill block -- [ ] 3.2 模型用戶可選 + 品質門檻白名單(預設 Haiku,深萃 Claude via CC) -- [ ] 3.3 模型測試集(中文 + 人類暗示樣本,轉回歸測試)— deferred,先跑預設 -- [ ] 3.4 JSON-fail 升級閘(淺萃失敗升 deep) -- [ ] 3.5 第一版不 embed(embed 等 base vectorize,InkStoneCo T2.4) +- [x] 3.1 cherry-pick `wiki_synthesis.yaml` classify 模式 → extract prompt(JSON nodes[]+triplets[]) +- [x] 3.2 模型用戶可選(意圖非型號,LlmCaller 介面,預設 shallow/Haiku、deep/Claude via CC) +- [ ] 3.3 模型測試集(中文 + 人類暗示樣本,轉回歸測試)— **deferred**(先跑預設;護欄 + parse 已有單元測試) +- [x] 3.4 JSON-fail 升級閘(淺萃 fail/過稀 → 升 deep 一次) +- [x] 3.5 第一版不 embed(仍【打標】embed/predicate_embed 供未來 base 讀標;embed 動作等 Arcrun #7) +- [x] 3.x 端點對齊硬自檢護欄(`src/lib/endpoint-check.ts`,leo 壓測 14→0;自檢 + autoAlign 補齊) -## T4 跨 repo 織網(R4,主職) +## T4 跨 repo 織網(R4,主職)— `src/lib/weave.ts` -- [ ] 4.1 匯總多 repo 三元組 +- [x] 4.1 匯總多 repo 三元組 → 偵測跨庫橋(同名 node 跨 ≥2 repo)+ 異見(同 s/o 對、不同謂詞);**不算 bridge_score**(graph 領域,禁送) ## T5 輸出 + CLI(R5/R6) -- [ ] 5.1 POST envelope 給 graph `POST /triplets/ingest`(嚴格符合 contract)⏸ 待 graph 寫入端(InkStoneCo T3.3) -- [ ] 5.2 薄 ops CLI(手動重萃);不帶查詢 MCP +- [x] 5.1 POST envelope 給 graph `POST /triplets/ingest`(嚴格符合 contract;buildEnvelope strict + 顯式禁送欄位自檢提早攔)。對齊【full contract】(含 embed/id/aliases/predicate_embed,總管裁定 ingest 不退) +- [x] 5.2 薄 ops CLI(`scripts/ingest-cli.mjs`:refresh 經 Worker / pull dry-run);**不帶查詢 MCP** -## 阻擋項 +## 阻擋項 / 誠實標記 -1. ⏸ T5.1 依賴 graph `POST /triplets/ingest`(InkStoneCo T3,待 graph repo 實作)。 -2. ⏸ embed 依賴 base vectorize(InkStoneCo T2.4)。第一版不 embed 可先動。 +1. ⏸ **端到端 ingest→graph 走通**:graph receiver 已補對齊 full contract → 剩 ingest 部署 + `GRAPH_BASE_URL` 設定 → **待部署驗**,未假綠。 +2. ⏸ embed 依賴 base vectorize(Arcrun #7)。第一版不 embed(只打標)已動。 +3. T3.3 模型測試集 deferred;refresh 端 extract(Workers AI)第一版只走採取,深萃留 CLI/CC。 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..17ccb89 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2673 @@ +{ + "name": "kbdb-ingest-plugin", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kbdb-ingest-plugin", + "version": "0.1.0", + "dependencies": { + "@hono/zod-openapi": "^1.2.4", + "hono": "^4.7.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250219.0", + "typescript": "^5.7.0", + "vitest": "^3.1.0", + "wrangler": "^4.0.0" + } + }, + "node_modules/@asteasolutions/zod-to-openapi": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", + "license": "MIT", + "dependencies": { + "openapi3-ts": "^4.1.2" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260625.1.tgz", + "integrity": "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260625.1.tgz", + "integrity": "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260625.1.tgz", + "integrity": "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260625.1.tgz", + "integrity": "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260625.1.tgz", + "integrity": "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260626.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260626.1.tgz", + "integrity": "sha512-fBnpQyFRS3Ce1l2IUd3k+aUxgy/7VMlVXF4F672/eSrpXFeezCy3Ha6Z2uTyGgqu9sGvQPOj8nqKBv2yeI+ciw==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/zod-openapi": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@hono/zod-openapi/-/zod-openapi-1.4.0.tgz", + "integrity": "sha512-AFchqR1N/NxfI4hUOSGI2/g8zLROxA1OE7Oh5JJFlTaGxhrdRyH+93gd0tIBpb0z8s9r8hUoNnaOBfHbdb4NMw==", + "license": "MIT", + "dependencies": { + "@asteasolutions/zod-to-openapi": "^8.5.0", + "@hono/zod-validator": "^0.8.0", + "openapi3-ts": "^4.5.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "hono": ">=4.10.0", + "zod": "^4.0.0" + } + }, + "node_modules/@hono/zod-validator": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@hono/zod-validator/-/zod-validator-0.8.0.tgz", + "integrity": "sha512-5uS4S1/LKtZQYvD4BtpPUFkOv8d1wNxHHrChm26buMiEYc1FrHWvDUaKVBwkiVtvSExHSpLGDvcnpI2Copyj9w==", + "license": "MIT", + "peerDependencies": { + "hono": ">=4.10.0", + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/miniflare": { + "version": "4.20260625.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260625.0.tgz", + "integrity": "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.28.0", + "workerd": "1.20260625.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/openapi3-ts": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.6.0.tgz", + "integrity": "sha512-a4sfn6L2sIShhtzJqmjGrARvxAW/3F2BJDdyRVvNF9VhAsZSh5hSyI3a9TNvmzBxXmq66nY5LNT5bQcBxYAZZg==", + "license": "MIT", + "dependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "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" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/workerd": { + "version": "1.20260625.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260625.1.tgz", + "integrity": "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260625.1", + "@cloudflare/workerd-darwin-arm64": "1.20260625.1", + "@cloudflare/workerd-linux-64": "1.20260625.1", + "@cloudflare/workerd-linux-arm64": "1.20260625.1", + "@cloudflare/workerd-windows-64": "1.20260625.1" + } + }, + "node_modules/wrangler": { + "version": "4.105.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.105.0.tgz", + "integrity": "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260625.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260625.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260625.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "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 + } + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f77ed2e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "kbdb-ingest-plugin", + "version": "0.1.0", + "private": true, + "description": "KBDB-ingest 插件:純餵食器——GitHub 拉 + 採取/萃取三元組候選 + 跨庫織網 → POST envelope 給 kbdb-graph-plugin。不碰儲存。", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "test": "vitest run", + "test:watch": "vitest", + "ingest": "node scripts/ingest-cli.mjs" + }, + "dependencies": { + "@hono/zod-openapi": "^1.2.4", + "hono": "^4.7.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20250219.0", + "typescript": "^5.7.0", + "vitest": "^3.1.0", + "wrangler": "^4.0.0" + } +} diff --git a/scripts/ingest-cli.mjs b/scripts/ingest-cli.mjs new file mode 100644 index 0000000..2faf565 --- /dev/null +++ b/scripts/ingest-cli.mjs @@ -0,0 +1,117 @@ +#!/usr/bin/env node +// 薄 ops CLI(T5.2)— 人手動觸發重萃。不帶查詢 MCP(ambient 餵食器沒人「問」它)。 +// +// 兩種模式: +// ingest refresh 經部署的 Worker /refresh 重萃單一來源 +// ingest pull [root] 本地 dry-run:拉 + 列出會送的 envelope(不 POST) +// +// 設定走 env: +// KBDB_INGEST_URL 已部署的 ingest Worker base(refresh 模式用) +// GRAPH_BASE_URL graph 寫入端(pull --post 用) +// GITHUB_TOKEN 拉私庫用(公庫可空) +// +// 鐵律:CLI 不碰儲存;refresh 經 Worker、pull --post 經 graph 寫入端。觸發=人手動(無排程)。 + +import process from 'node:process'; + +const [, , cmd, arg, arg2] = process.argv; + +async function sha256hex(text) { + const data = new TextEncoder().encode(text); + const digest = await crypto.subtle.digest('SHA-256', data); + return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +function ghHeaders() { + const h = { Accept: 'application/vnd.github+json', 'User-Agent': 'kbdb-ingest-cli' }; + if (process.env.GITHUB_TOKEN) h.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`; + return h; +} + +async function ghGetFile(owner, repo, path) { + const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}`; + const res = await fetch(url, { headers: ghHeaders() }); + if (!res.ok) throw new Error(`github ${owner}/${repo}@${path}: ${res.status}`); + const body = await res.json(); + const text = body.encoding === 'base64' ? Buffer.from(body.content, 'base64').toString('utf-8') : body.content; + return { text, commit: body.sha }; +} + +async function ghListMarkdown(owner, repo, root = '') { + const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`, { headers: ghHeaders() }); + if (!res.ok) throw new Error(`github list ${owner}/${repo}: ${res.status}`); + const body = await res.json(); + const prefix = root.replace(/^\/+|\/+$/g, ''); + return (body.tree || []) + .filter((e) => e.type === 'blob' && e.path.endsWith('.md')) + .map((e) => e.path) + .filter((p) => (prefix ? p === prefix || p.startsWith(prefix + '/') : true)); +} + +// 極簡採取(鏡射 src/lib/harvest.ts;CLI dry-run 用,不引 TS)。 +function harvest(md) { + const fm = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(md); + const body = fm ? fm[2] : md; + const gloss = fm && /^gloss:\s*(.+)$/m.exec(fm[1]) ? /^gloss:\s*(.+)$/m.exec(fm[1])[1].trim() : undefined; + const title = /^#\s+(.+)$/m.exec(body)?.[1]?.trim(); + const sec = (h) => new RegExp(`^##\\s+${h}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, 'm').exec(body)?.[1] || ''; + const nodes = []; + if (title) nodes.push({ name: title, gloss, embed: true }); + for (const line of sec('實體').split('\n')) { + const m = /^-\s*\*\*(.+?)\*\*\s*(?:((.+?)))?\s*(?:[—-]\s*(.+))?$/.exec(line.trim()); + if (m) nodes.push({ name: m[1].trim(), gloss: m[3]?.trim() || undefined, embed: true }); + } + const triplets = []; + for (const line of sec('關聯').split('\n')) { + const m = /^(.+?)\s*>>\s*(.+?)\s*>>\s*(.+?)$/.exec(line.replace(/^-\s*/, '').trim()); + if (m) { + const clean = (s) => s.replace(/\[\[|\]\]|\*\*/g, '').trim(); + triplets.push({ subject: clean(m[1]), predicate: m[2].trim(), object: clean(m[3]), predicate_embed: true }); + } + } + return { nodes, triplets }; +} + +async function doRefresh(uri) { + const base = process.env.KBDB_INGEST_URL; + if (!base) throw new Error('KBDB_INGEST_URL 未設(指向已部署的 ingest Worker)'); + const res = await fetch(base.replace(/\/$/, '') + '/refresh', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ uri }), + }); + console.log(JSON.stringify(await res.json(), null, 2)); +} + +async function doPull(ownerRepo, root) { + const [owner, repo] = ownerRepo.split('/'); + if (!owner || !repo) throw new Error('用法:ingest pull [root]'); + const paths = await ghListMarkdown(owner, repo, root || ''); + console.error(`[ingest] ${owner}/${repo}: ${paths.length} 個 MD`); + const envelopes = []; + for (const path of paths) { + const { text, commit } = await ghGetFile(owner, repo, path); + const { nodes, triplets } = harvest(text); + if (!triplets.length) continue; // 採不到(非 template 卡)→ dry-run 跳過(CLI 不做 extract) + envelopes.push({ + source: { uri: `github:${owner}/${repo}@${path}`, content_hash: await sha256hex(text), commit }, + extractor: { model: 'local-harvest', tier: 'shallow' }, + nodes, + triplets, + }); + } + console.error(`[ingest] 採取出 ${envelopes.length} 個 envelope(共 ${envelopes.reduce((n, e) => n + e.triplets.length, 0)} 三元組)`); + console.log(JSON.stringify(envelopes, null, 2)); +} + +try { + if (cmd === 'refresh' && arg) await doRefresh(arg); + else if (cmd === 'pull' && arg) await doPull(arg, arg2); + else { + console.error('用法:\n ingest refresh \n ingest pull [root]'); + process.exit(2); + } +} catch (e) { + console.error('[ingest] 錯誤:', e.message); + process.exit(1); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..12c660d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,87 @@ +// KBDB-ingest 插件 Worker 進入點 — 純餵食器。 +// +// 鐵律:不碰儲存(無 D1/Vectorize/AI 綁定)。只 POST envelope 給 graph 寫入端。 +// 端點:/refresh = graph 的 POST /graph/refresh 代轉過來的受理端(人發起、非自動 fan-out)。 +// refresh 收到 {uri, owner_id} → 拉該來源 → 採取/萃取 → POST envelope 給 graph。 +// 不帶查詢 MCP(ambient 餵食器);ops 走薄 CLI(scripts/ingest-cli.mjs)。 + +import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'; +import { cors } from 'hono/cors'; +import type { Bindings, Variables } from './types'; +import { makeGitHubFetcher, parseSourceUri, contentHash, makeSourceUri } from './lib/source-adapter'; +import { processSource } from './lib/pipeline'; +import { makeGraphClient } from './lib/graph-client'; + +const app = new OpenAPIHono<{ Bindings: Bindings; Variables: Variables }>(); + +app.onError((err, c) => { + console.error(err); + return c.json({ error: 'Internal Server Error', message: err.message }, 500); +}); + +app.use('*', cors({ origin: '*', allowHeaders: ['Content-Type', 'Authorization'], allowMethods: ['GET', 'POST', 'OPTIONS'] })); + +app.get('/', (c) => c.json({ service: 'kbdb-ingest', tier: 'plugin', role: 'feeder', status: 'ok' })); +app.get('/health', (c) => + c.json({ service: 'kbdb-ingest', status: 'ok', graph_url_set: Boolean(c.env.GRAPH_BASE_URL) }), +); + +// POST /refresh — graph 代轉重萃某來源。被動:收一次調用 → 處理一次(無排程/webhook)。 +const refreshRoute = createRoute({ + method: 'post', + path: '/refresh', + request: { + body: { + content: { + 'application/json': { + schema: z.object({ + uri: z.string().min(1).describe("github:owner/repo@path"), + owner_id: z.string().optional(), + }), + }, + }, + }, + }, + responses: { + 200: { description: 'Refreshed: pulled, harvested/extracted, posted envelope to graph' }, + 400: { description: 'Bad uri' }, + }, + tags: ['Ingest'], +}); + +app.openapi(refreshRoute, async (c) => { + const { uri } = c.req.valid('json'); + const parsed = parseSourceUri(uri); + if (!parsed) return c.json({ error: 'uri 須為 github:owner/repo@path' }, 400); + + const fetcher = makeGitHubFetcher(c.env.GITHUB_TOKEN); + const { text, commit } = await fetcher.getFile(parsed.owner, parsed.repo, parsed.path); + const file = { + uri: makeSourceUri(parsed.owner, parsed.repo, parsed.path), + path: parsed.path, + text, + content_hash: await contentHash(text), + commit, + }; + + // 第一版 refresh 只走採取(路徑 A);extract 模型在 Worker runtime 接 Workers AI 是後續 + // (CLI 端可帶 deep via CC)。採不到三元組 → 誠實回 skipped,不假萃。 + const result = await processSource(file); + if (!result.envelope) { + return c.json({ refreshed: false, path: result.path, note: result.note }, 200); + } + + const graph = makeGraphClient(c.env.GRAPH_BASE_URL, c.env.GRAPH_INTERNAL_TOKEN); + const post = await graph.postEnvelope(result.envelope); + return c.json( + { + refreshed: post.ok, + path: result.path, + triplets: result.envelope.triplets.length, + graph: post.ok ? post.body : { status: post.status, error: post.error, issues: (post.body as any)?.issues }, + }, + 200, + ); +}); + +export default app; diff --git a/src/lib/endpoint-check.ts b/src/lib/endpoint-check.ts new file mode 100644 index 0000000..a2e0f6b --- /dev/null +++ b/src/lib/endpoint-check.ts @@ -0,0 +1,49 @@ +// 端點對齊硬自檢護欄(leo 真 vault 壓測實證:光寫規則 Haiku 會略過,端點對不齊 14 條; +// 寫成自檢動作後 14→0)。 +// +// 規則:每條內文三元組的 subject/object 必須對得上某個 node 名(一字不差)。 +// 對不齊 = 下游圖斷鏈(端點 match 不到 node)。本護欄在 envelope 出門前機械檢, +// 撈出對不齊的端點,呼叫端可選擇修補 / 丟棄 / warn。 + +import type { EnvelopeEdge, EnvelopeNode } from '../types'; + +export interface AlignmentReport { + aligned: boolean; + /** 對不齊的端點描述(給人讀 / log)。 */ + unaligned: string[]; +} + +/** + * 檢查三元組端點是否都對得上 nodes[].name。 + * 卡對卡端點(原文 `[[卡]]`)已在 harvest 去括號 → 一律以裸名比對。 + */ +export function checkEndpointAlignment(nodes: EnvelopeNode[], triplets: EnvelopeEdge[]): AlignmentReport { + const names = new Set(nodes.map((n) => n.name)); + const unaligned: string[] = []; + for (const t of triplets) { + for (const [role, ep] of [['subject', t.subject], ['object', t.object]] as const) { + if (!names.has(ep)) { + unaligned.push(`${role}「${ep}」對不齊(${t.subject} >> ${t.predicate} >> ${t.object})`); + } + } + } + return { aligned: unaligned.length === 0, unaligned }; +} + +/** + * 自動補齊:對不齊的端點,把它當成新 node 補進 nodes[](embed:true,無 gloss)。 + * 比丟棄三元組保守——保住邊,下游仍可 normalize。回傳補過的 nodes。 + */ +export function autoAlignEndpoints(nodes: EnvelopeNode[], triplets: EnvelopeEdge[]): EnvelopeNode[] { + const names = new Set(nodes.map((n) => n.name)); + const out = [...nodes]; + for (const t of triplets) { + for (const ep of [t.subject, t.object]) { + if (!names.has(ep)) { + names.add(ep); + out.push({ name: ep, embed: true }); + } + } + } + return out; +} diff --git a/src/lib/envelope.ts b/src/lib/envelope.ts new file mode 100644 index 0000000..14ce376 --- /dev/null +++ b/src/lib/envelope.ts @@ -0,0 +1,51 @@ +// envelope 組裝 + 出門前禁送欄位自檢。 +// +// 一個 envelope = 一個來源檔一次萃取的產物(契約定義)。組裝後跑 EnvelopeSchema 驗證 +// (strict → 多帶禁送欄位會 throw,提早在 ingest 端攔,不等 graph 422)。 + +import { + EnvelopeSchema, + FORBIDDEN_EDGE_KEYS, + FORBIDDEN_TOP_KEYS, + type Envelope, + type EnvelopeEdge, + type EnvelopeNode, +} from '../types'; + +export interface BuildEnvelopeInput { + source: { uri: string; content_hash: string; anchor?: string; commit?: string; block_id?: string }; + extractor: { model: string; tier: 'shallow' | 'deep'; extracted_at?: number }; + nodes?: EnvelopeNode[]; + triplets: EnvelopeEdge[]; +} + +/** + * 組 envelope 並驗證(strict)。 + * - 結構符號/散文不該進;nodes/triplets 由上游(harvest/extract)已過濾。 + * - 驗證失敗(多帶禁送欄位、形狀錯)→ throw ZodError,呼叫端攔(比送出去被 graph 422 早)。 + */ +export function buildEnvelope(input: BuildEnvelopeInput): Envelope { + // 顯式禁送欄位自檢(除了 strict schema,多一道明確攔——上游若塞 graph 領域欄位提早炸)。 + for (const n of input.nodes ?? []) { + for (const k of [...FORBIDDEN_TOP_KEYS, 'clusters']) { + if (k !== 'id' && k in (n as Record)) { + throw new Error(`envelope: node「${n.name}」帶禁送欄位 ${k}(graph 領域,ingest 不可送)`); + } + } + } + for (const t of input.triplets) { + for (const k of FORBIDDEN_EDGE_KEYS) { + if (k in (t as Record)) { + throw new Error(`envelope: 邊「${t.subject}>>${t.object}」帶禁送欄位 ${k}(類型只走 nodes[])`); + } + } + } + const candidate: Envelope = { + source: input.source, + extractor: input.extractor, + triplets: input.triplets, + ...(input.nodes && input.nodes.length ? { nodes: input.nodes } : {}), + }; + // strict 驗證:等於本地版「禁送欄位 → 擋」。throw 給呼叫端。 + return EnvelopeSchema.parse(candidate); +} diff --git a/src/lib/extract.ts b/src/lib/extract.ts new file mode 100644 index 0000000..9c11b34 --- /dev/null +++ b/src/lib/extract.ts @@ -0,0 +1,110 @@ +// T3 extract(路徑 B,fallback)— 裸原文無本地三元組時,ingest 自己萃 (s,p,o)+gloss。 +// +// 模型用戶可選(意圖非型號):shallow=Haiku/Workers AI(預設、便宜);deep=Claude via CC(深萃、走月費)。 +// JSON-fail 升級閘:shallow 解析失敗 / 萃太稀 → 升 deep 重萃一次。 +// 第一版不 embed(embed 等 base vectorize / Arcrun #7)——但仍【打標】embed/predicate_embed 供未來讀標。 +// 端點對齊護欄:萃完用 endpoint-check 自檢 + 自動補齊(leo 壓測 14→0)。 +// +// LLM 呼叫抽象成 LlmCaller 介面 → 測試走 mock,不打網路、不花錢。 + +import type { EnvelopeEdge, EnvelopeNode } from '../types'; +import { autoAlignEndpoints, checkEndpointAlignment } from './endpoint-check'; + +export type ExtractTier = 'shallow' | 'deep'; + +export interface ExtractedGraph { + nodes: EnvelopeNode[]; + triplets: EnvelopeEdge[]; +} + +/** 一次 LLM 萃取呼叫。回傳模型【原始文字】(期望是 JSON),由本模組負責 parse。 */ +export interface LlmCaller { + /** model = 解析後的具體型號字串(供 extractor.model 記錄)。 */ + readonly model: string; + call(prompt: string, text: string): Promise; +} + +export interface ExtractResult extends ExtractedGraph { + tier: ExtractTier; + model: string; + /** 是否因 shallow JSON-fail/過稀而升級到 deep。 */ + escalated: boolean; +} + +const EXTRACT_PROMPT = `你是知識圖譜萃取器。讀下面的原文,萃出三元組與實體。嚴格輸出 JSON(繁體中文內容),格式: +{ + "nodes": [{"name": "正規名", "gloss": "一句話定義(這個實體是什麼)", "aliases": ["同義詞"]}], + "triplets": [{"subject": "主詞", "predicate": "動詞短語", "object": "受詞", "confidence": 0.0-1.0}] +} +規則: +- 謂詞用動詞/動詞短語(如「奠基於」「反駁」),禁名詞當謂詞。 +- triplet 的 subject/object 必須對得上某個 nodes[].name(一字不差)。 +- 抓深層暗示,不只表面陳述。只輸出 JSON,不要其他文字。`; + +/** 解析模型輸出的 JSON(容忍 ```json fenced 區塊)。失敗 throw。 */ +export function parseExtractJson(raw: string): ExtractedGraph { + const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(raw); + const jsonText = (fenced ? fenced[1] : raw).trim(); + const parsed = JSON.parse(jsonText) as Partial; + if (!Array.isArray(parsed.triplets) || parsed.triplets.length === 0) { + throw new Error('extract: no triplets in model output'); + } + const nodes: EnvelopeNode[] = (parsed.nodes ?? []).map((n) => ({ + name: String(n.name), + gloss: n.gloss ? String(n.gloss) : undefined, + aliases: Array.isArray(n.aliases) ? n.aliases.map(String) : undefined, + embed: true, // 打標 true(base 讀標執行;第一版 base 還沒接,標仍合契約) + })); + const triplets: EnvelopeEdge[] = parsed.triplets.map((t) => ({ + subject: String(t.subject), + predicate: String(t.predicate), + object: String(t.object), + confidence: typeof t.confidence === 'number' ? t.confidence : undefined, + predicate_embed: true, + })); + return { nodes, triplets }; +} + +/** 萃太稀(門檻)→ 視為失敗、觸發升級。 */ +function tooSparse(g: ExtractedGraph): boolean { + return g.triplets.length < 1; +} + +/** + * extract:先用 shallowCaller 淺萃;JSON-fail 或過稀 → 若有 deepCaller 升級重萃一次。 + * 萃完跑端點對齊護欄並自動補齊。deepCaller 省略 = 不升級(純 shallow)。 + */ +export async function extract( + text: string, + shallowCaller: LlmCaller, + deepCaller?: LlmCaller, +): Promise { + let tier: ExtractTier = 'shallow'; + let model = shallowCaller.model; + let graph: ExtractedGraph | null = null; + let escalated = false; + + try { + graph = parseExtractJson(await shallowCaller.call(EXTRACT_PROMPT, text)); + if (tooSparse(graph)) throw new Error('extract: shallow too sparse'); + } catch { + graph = null; + } + + if (!graph && deepCaller) { + escalated = true; + tier = 'deep'; + model = deepCaller.model; + graph = parseExtractJson(await deepCaller.call(EXTRACT_PROMPT, text)); // deep 失敗就 throw 給呼叫端 + } + + if (!graph) throw new Error('extract: shallow failed and no deep caller to escalate'); + + // 端點對齊護欄(leo 壓測必做):自檢 + 自動補齊(保住邊,不丟)。 + const aligned = autoAlignEndpoints(graph.nodes, graph.triplets); + const report = checkEndpointAlignment(aligned, graph.triplets); + // 補齊後理應全對齊;若仍有(理論上不會)留給呼叫端,但不阻斷。 + void report; + + return { nodes: aligned, triplets: graph.triplets, tier, model, escalated }; +} diff --git a/src/lib/graph-client.ts b/src/lib/graph-client.ts new file mode 100644 index 0000000..6df8376 --- /dev/null +++ b/src/lib/graph-client.ts @@ -0,0 +1,58 @@ +// T5 graph client — cherry-pick 自 polaris/mira/tools/_kbdb_client.py 的 HTTP-helper 模式, +// 但【改成純餵食器】:只 POST envelope 給 graph 寫入端,**不寫 base、不碰 D1/Vectorize/表**。 +// +// 原 _kbdb_client.py 直打 base /kbdb/entries(碰儲存)——那正是 ingest 鐵律禁止的。 +// 本檔保留它的「統一 http wrapper + header + 容錯回傳」骨架,把目標改成 graph 的 +// POST /triplets/ingest(API-as-Wall:ingest 只透過 graph HTTP 寫入端餵候選)。 + +import type { Envelope } from '../types'; + +export interface PostResult { + ok: boolean; + /** graph 回的 {skipped,ingested,deprecated}(200);422/未設時 ok=false。 */ + status: number; + body?: unknown; + error?: string; +} + +export interface GraphClient { + postEnvelope(env: Envelope): Promise; +} + +/** + * 真實 graph client。baseUrl 空 → 誠實回 {ok:false, error:'GRAPH_BASE_URL 未設'},不假綠 + * (對齊 graph 端 refresh「未設 ingest URL 誠實回 forwarded:false」的誠實原則)。 + */ +export function makeGraphClient( + baseUrl: string | undefined, + token?: string, + fetchImpl: typeof fetch = fetch, +): GraphClient { + return { + async postEnvelope(env) { + if (!baseUrl) { + return { ok: false, status: 0, error: 'GRAPH_BASE_URL 未設:graph 寫入端尚未就緒/未部署,envelope 無對象可送。' }; + } + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers.Authorization = `Bearer ${token}`; + const url = baseUrl.replace(/\/$/, '') + '/triplets/ingest'; + let res: Response; + try { + res = await fetchImpl(url, { method: 'POST', headers, body: JSON.stringify(env) }); + } catch (e) { + return { ok: false, status: 0, error: `[graph] POST ${url}: ${(e as Error).message}` }; + } + let body: unknown; + try { + body = await res.json(); + } catch { + body = undefined; + } + // 422 = envelope 違規(禁送欄位/形狀)→ 不 ok,帶 graph 回的 issues 供修。 + if (!res.ok) { + return { ok: false, status: res.status, body, error: `graph ${res.status} ${res.statusText}` }; + } + return { ok: true, status: res.status, body }; + }, + }; +} diff --git a/src/lib/harvest.ts b/src/lib/harvest.ts new file mode 100644 index 0000000..d310855 --- /dev/null +++ b/src/lib/harvest.ts @@ -0,0 +1,146 @@ +// T2 採取(路徑 A,優先)— 從 system-dev-template 1.8.0+ 的 wiki 卡採取已建好的三元組+gloss。 +// +// 本地萃成效更好(知識連結長在生產當下、有 LLM Wiki 指引),ingest 優先採取、不重萃。 +// 解析卡片格式(與本 repo system-dev/wiki/cards 同源): +// frontmatter: gloss:(卡標題 node 的描述) +// ## 實體:一行一個 `- **正規名**(aliases…)— 描述句`(內文 node + gloss) +// ## 關聯:typed-edge `A >> 謂詞 >> B`(內文裸文字端點)/ `[[卡]] >> 謂詞 >> [[卡]]`(卡對卡) +// +// 鐵律:結構符號(>>/←)與散文(## 摘要)不進 envelope。打標 embed/predicate_embed(預設 true)。 + +import type { EnvelopeEdge, EnvelopeNode } from '../types'; + +export interface HarvestResult { + nodes: EnvelopeNode[]; + triplets: EnvelopeEdge[]; + /** 端點對不齊 `## 實體` 的三元組(自檢護欄;見 endpoint-check.ts 用此 warn)。 */ + unalignedEndpoints: string[]; +} + +interface Frontmatter { + gloss?: string; + tags?: string[]; +} + +/** 抽 frontmatter(--- … ---)。簡單 YAML,只取 gloss / tags。 */ +export function parseFrontmatter(md: string): { fm: Frontmatter; body: string } { + const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(md); + if (!m) return { fm: {}, body: md }; + const fm: Frontmatter = {}; + for (const line of m[1].split('\n')) { + const g = /^gloss:\s*(.+)$/.exec(line.trim()); + if (g) fm.gloss = g[1].replace(/^["']|["']$/g, '').trim(); + } + return { fm, body: m[2] }; +} + +/** 取卡標題(首個 # H1)。 */ +export function parseTitle(body: string): string | null { + const m = /^#\s+(.+)$/m.exec(body); + return m ? m[1].trim() : null; +} + +/** 抽某 H2 段落內文(到下個 H2 或檔尾)。H3 子節(### …)仍算段內。 */ +function section(body: string, heading: string): string | null { + // 不用 m 旗標(避免 $ 在每行尾命中);終止 = 下個 `\n## `(H2,非 H3)或字串尾。 + const re = new RegExp(`(?:^|\\n)##\\s+${heading}[^\\n]*\\n([\\s\\S]*?)(?=\\n##\\s|$)`); + const m = re.exec(body); + return m ? m[1] : null; +} + +/** 解析 `## 實體` 行:`- **正規名**(alias1/alias2)— 描述句`。 */ +export function parseEntities(body: string): EnvelopeNode[] { + const sec = section(body, '實體'); + if (!sec) return []; + const out: EnvelopeNode[] = []; + for (const raw of sec.split('\n')) { + const line = raw.trim(); + if (!line.startsWith('-')) continue; + // - **名**(aliases)— gloss 或 - **名** — gloss 或 - **名** + const m = /^-\s*\*\*(.+?)\*\*\s*(?:((.+?)))?\s*(?:[—-]\s*(.+))?$/.exec(line); + if (!m) continue; + const name = m[1].trim(); + // 別名分隔用全形「/」「、」(template 慣例);ASCII '/' 不切(如 arcrun/kbdb 是一個別名)。 + const aliases = m[2] + ? m[2].split(/[/、]/).map((s) => s.trim()).filter(Boolean) + : undefined; + const gloss = m[3]?.trim() || undefined; + const node: EnvelopeNode = { name, embed: true }; + if (gloss) node.gloss = gloss; + if (aliases && aliases.length) node.aliases = aliases; + out.push(node); + } + return out; +} + +/** 一條解析出的邊 + 它的兩端是否為卡對卡(原文帶 [[ ]])。 */ +export interface ParsedEdge extends EnvelopeEdge { + /** subject 端原文是 [[wikilink]](卡對卡,不要求對齊 ## 實體)。 */ + subjectIsCard: boolean; + objectIsCard: boolean; +} + +/** 解析 typed-edge 行 `A >> 謂詞 >> B`(sep 可設,預設 >>)。端點去 `[[ ]]`、`**`。 */ +export function parseEdges(body: string, sep = '>>'): ParsedEdge[] { + const sec = section(body, '關聯'); + if (!sec) return []; + const out: ParsedEdge[] = []; + const escSep = sep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const re = new RegExp(`^(.+?)\\s*${escSep}\\s*(.+?)\\s*${escSep}\\s*(.+?)$`); + for (const raw of sec.split('\n')) { + const line = raw.trim(); + if (!line.startsWith('-')) continue; + const m = re.exec(line.replace(/^-\s*/, '')); + if (!m) continue; + const clean = (s: string) => s.replace(/\[\[|\]\]/g, '').replace(/\*\*/g, '').trim(); + out.push({ + subject: clean(m[1]), + predicate: m[2].trim(), + object: clean(m[3]), + predicate_embed: true, + subjectIsCard: /\[\[.+?\]\]/.test(m[1]), + objectIsCard: /\[\[.+?\]\]/.test(m[3]), + }); + } + return out; +} + +/** 採取單張卡 → nodes + triplets(含卡標題 node 的 frontmatter gloss)。 */ +export function harvestCard(md: string): HarvestResult { + const { fm, body } = parseFrontmatter(md); + const title = parseTitle(body); + const nodes = parseEntities(body); + + // 卡標題本身是個 node(wikilink 卡)。frontmatter gloss 描述它。 + if (title && !nodes.some((n) => n.name === title)) { + const cardNode: EnvelopeNode = { name: title, embed: true }; + if (fm.gloss) cardNode.gloss = fm.gloss; + nodes.unshift(cardNode); + } + + const parsed = parseEdges(body); + + // 卡對卡端點(原文 [[卡]])也是 graph node(被連到的卡)→ 補進 nodes(embed:true,無 gloss)。 + // 這樣它們對齊、且下游知道有這些卡 node。 + const nodeNames = new Set(nodes.map((n) => n.name)); + for (const e of parsed) { + if (e.subjectIsCard && !nodeNames.has(e.subject)) { nodeNames.add(e.subject); nodes.push({ name: e.subject, embed: true }); } + if (e.objectIsCard && !nodeNames.has(e.object)) { nodeNames.add(e.object); nodes.push({ name: e.object, embed: true }); } + } + + // 端點對齊自檢(leo 壓測護欄):內文三元組端點(非卡對卡)須對得上某 node 名。 + const unalignedEndpoints: string[] = []; + for (const e of parsed) { + if (!e.subjectIsCard && !nodeNames.has(e.subject)) + unalignedEndpoints.push(`${e.subject}(在「${e.subject} >> ${e.predicate} >> ${e.object}」)`); + if (!e.objectIsCard && !nodeNames.has(e.object)) + unalignedEndpoints.push(`${e.object}(在「${e.subject} >> ${e.predicate} >> ${e.object}」)`); + } + + // 去掉 ParsedEdge 的 isCard 標記 → 純 EnvelopeEdge。 + const triplets: EnvelopeEdge[] = parsed.map(({ subject, predicate, object, predicate_embed, confidence }) => ({ + subject, predicate, object, predicate_embed, ...(confidence !== undefined ? { confidence } : {}), + })); + + return { nodes, triplets, unalignedEndpoints }; +} diff --git a/src/lib/pipeline.ts b/src/lib/pipeline.ts new file mode 100644 index 0000000..9066a16 --- /dev/null +++ b/src/lib/pipeline.ts @@ -0,0 +1,59 @@ +// 編排:source → 採取(路徑A優先) / 萃取(路徑B fallback) → envelope。 +// +// 每個 SourceFile 出一個 envelope(契約:一檔一 envelope)。採取優先:卡有三元組就採; +// 採不到(無 ## 關聯 / 非 template 卡)才走 extract。跨 repo 織網在更上層(weave)匯總。 + +import { harvestCard } from './harvest'; +import { extract, type LlmCaller } from './extract'; +import { buildEnvelope } from './envelope'; +import type { SourceFile } from './source-adapter'; +import type { Envelope } from '../types'; + +export interface ProcessOptions { + shallowCaller?: LlmCaller; + deepCaller?: LlmCaller; + /** 採取(路徑 A)模型標記,記進 extractor.model。預設 'local-harvest'。 */ + harvestModel?: string; +} + +export interface ProcessResult { + envelope: Envelope | null; + path: 'harvest' | 'extract' | 'skipped'; + note?: string; +} + +/** 採取結果是否「夠」(有三元組)→ 不必 fallback 到 extract。 */ +function harvestSufficient(triplets: unknown[]): boolean { + return triplets.length > 0; +} + +/** 處理單一來源檔 → envelope(採取優先,採不到 fallback extract)。 */ +export async function processSource(file: SourceFile, opts: ProcessOptions = {}): Promise { + // 路徑 A:採取本地已建三元組+gloss。 + const harvested = harvestCard(file.text); + if (harvestSufficient(harvested.triplets)) { + const envelope = buildEnvelope({ + source: { uri: file.uri, content_hash: file.content_hash, commit: file.commit }, + extractor: { model: opts.harvestModel ?? 'local-harvest', tier: 'shallow' }, + nodes: harvested.nodes, + triplets: harvested.triplets, + }); + const note = harvested.unalignedEndpoints.length + ? `採取:${harvested.unalignedEndpoints.length} 端點對不齊(已留 node)` + : undefined; + return { envelope, path: 'harvest', note }; + } + + // 路徑 B:裸原文 extract(需 shallowCaller)。 + if (!opts.shallowCaller) { + return { envelope: null, path: 'skipped', note: '無本地三元組且未提供萃取模型 → 跳過' }; + } + const ex = await extract(file.text, opts.shallowCaller, opts.deepCaller); + const envelope = buildEnvelope({ + source: { uri: file.uri, content_hash: file.content_hash, commit: file.commit }, + extractor: { model: ex.model, tier: ex.tier }, + nodes: ex.nodes, + triplets: ex.triplets, + }); + return { envelope, path: 'extract', note: ex.escalated ? '淺萃失敗 → 升 deep' : undefined }; +} diff --git a/src/lib/source-adapter.ts b/src/lib/source-adapter.ts new file mode 100644 index 0000000..a57d334 --- /dev/null +++ b/src/lib/source-adapter.ts @@ -0,0 +1,108 @@ +// T1 SourceAdapter — 從 GitHub 拉 repo 的 MD 檔 + per-file content-hash。 +// +// 鐵律:runtime 用 GitHub API 拉 repo(不開 Actions、不掛 webhook 自動同步)。 +// 拉是 runtime 行為(人/refresh 發起的一次調用),不衝突 flag 紅線。 +// source.uri = 'github:/@'(穩定識別 = 快照鍵 + get_source 指標)。 + +export interface SourceFile { + /** github:owner/repo@path */ + uri: string; + /** 檔內相對路徑(owner/repo 之外的部分)。 */ + path: string; + /** 原始檔內容(UTF-8)。 */ + text: string; + /** content_hash(sha256 hex,快照鍵)。 */ + content_hash: string; + /** git commit sha(可追溯,選填)。 */ + commit?: string; +} + +/** sha256 hex —— Workers 與 Node 18+ 皆有 crypto.subtle。 */ +export async function contentHash(text: string): Promise { + const data = new TextEncoder().encode(text); + const digest = await crypto.subtle.digest('SHA-256', data); + return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +/** 組 source.uri(單一真相格式,全程經此函式產,避免拼錯)。 */ +export function makeSourceUri(owner: string, repo: string, path: string): string { + return `github:${owner}/${repo}@${path}`; +} + +/** 解析 source.uri 回 {owner, repo, path}。null = 格式不符。 */ +export function parseSourceUri(uri: string): { owner: string; repo: string; path: string } | null { + const m = /^github:([^/]+)\/([^@]+)@(.+)$/.exec(uri); + if (!m) return null; + return { owner: m[1], repo: m[2], path: m[3] }; +} + +export interface GitHubFetcher { + /** 列出 repo 內某路徑下的 MD 檔(遞迴)。回傳檔路徑 list。 */ + listMarkdown(owner: string, repo: string, root?: string): Promise; + /** 取單檔原文 + commit sha。 */ + getFile(owner: string, repo: string, path: string): Promise<{ text: string; commit?: string }>; +} + +/** + * 真實 GitHub API fetcher(runtime 拉,非 Actions)。 + * token 選填:公庫可不帶;私庫帶 GITHUB_TOKEN。測試走 mock,不打網路。 + */ +export function makeGitHubFetcher(token?: string, fetchImpl: typeof fetch = fetch): GitHubFetcher { + const headers: Record = { + Accept: 'application/vnd.github+json', + 'User-Agent': 'kbdb-ingest-plugin', + }; + if (token) headers.Authorization = `Bearer ${token}`; + + const api = 'https://api.github.com'; + + return { + async listMarkdown(owner, repo, root = '') { + // git/trees 遞迴:一次 API call 拿整棵樹(避免逐目錄 fan-out 流量)。 + const res = await fetchImpl(`${api}/repos/${owner}/${repo}/git/trees/HEAD?recursive=1`, { headers }); + if (!res.ok) throw new Error(`[github] list ${owner}/${repo}: ${res.status} ${res.statusText}`); + const body = (await res.json()) as { tree?: Array<{ path: string; type: string }> }; + const prefix = root.replace(/^\/+|\/+$/g, ''); + return (body.tree ?? []) + .filter((e) => e.type === 'blob' && e.path.endsWith('.md')) + .map((e) => e.path) + .filter((p) => (prefix ? p === prefix || p.startsWith(prefix + '/') : true)); + }, + async getFile(owner, repo, path) { + const res = await fetchImpl(`${api}/repos/${owner}/${repo}/contents/${encodeURIComponent(path).replace(/%2F/g, '/')}`, { headers }); + if (!res.ok) throw new Error(`[github] get ${owner}/${repo}@${path}: ${res.status} ${res.statusText}`); + const body = (await res.json()) as { content?: string; encoding?: string; sha?: string }; + const text = body.encoding === 'base64' && body.content ? decodeBase64Utf8(body.content) : (body.content ?? ''); + return { text, commit: body.sha }; + }, + }; +} + +function decodeBase64Utf8(b64: string): string { + const clean = b64.replace(/\n/g, ''); + const bin = atob(clean); + const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0)); + return new TextDecoder('utf-8').decode(bytes); +} + +/** 拉一個 repo 路徑下所有 MD → SourceFile[](含 content_hash)。 */ +export async function pullRepoMarkdown( + fetcher: GitHubFetcher, + owner: string, + repo: string, + root = '', +): Promise { + const paths = await fetcher.listMarkdown(owner, repo, root); + const out: SourceFile[] = []; + for (const path of paths) { + const { text, commit } = await fetcher.getFile(owner, repo, path); + out.push({ + uri: makeSourceUri(owner, repo, path), + path, + text, + content_hash: await contentHash(text), + commit, + }); + } + return out; +} diff --git a/src/lib/weave.ts b/src/lib/weave.ts new file mode 100644 index 0000000..11481a1 Binary files /dev/null and b/src/lib/weave.ts differ diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..44af04f --- /dev/null +++ b/src/types.ts @@ -0,0 +1,85 @@ +// 共用型別 + envelope 契約鏡射(contracts/ingest-candidate.json,full 版含向量化打標)。 +// +// 鐵律:ingest 純餵食器,只【打標】embed/predicate_embed + 帶 gloss/aliases; +// 實際 embedding 歸 base/KBDB embed 模組讀標執行。ingest 自己不算向量。 +// envelope 是 ingest↔graph 唯一耦合面(三守則:凍結契約)。 + +import { z } from '@hono/zod-openapi'; + +export interface Bindings { + ENVIRONMENT?: string; + /** graph 寫入端 base URL;空 = 未部署,POST 時誠實報 not-configured,不假綠。 */ + GRAPH_BASE_URL?: string; + /** 萃取預設 tier 意圖(shallow=Haiku;deep=Claude via CC)。 */ + DEFAULT_EXTRACT_TIER?: 'shallow' | 'deep'; + /** 拉 GitHub 私庫用(公庫可空)。走 secret put。 */ + GITHUB_TOKEN?: string; + /** graph 寫入端 bearer(對應 graph 的 KBDB_INTERNAL_TOKEN)。走 secret put。 */ + GRAPH_INTERNAL_TOKEN?: string; +} + +export interface Variables { + partner_id: string; +} + +// ── envelope 契約(full:含 ingest#1 升格的向量化打標欄位)────────────── +// graph 收件端 .strict() 追上 contract(graph#1 補對齊任務)後即收得下這些欄位。 + +export const EnvelopeNodeSchema = z + .object({ + name: z.string().min(1), + /** 去重鍵:wikilink 卡用檔名(一卡一 node,不以出現次數重複 embed);實體用正規名。 */ + id: z.string().optional(), + /** 一句話描述。base embed【名+gloss 一起】拉近同義詞。建議 deep tier 產。 */ + gloss: z.string().optional(), + /** 同義詞(黃仁勳/Jensen Huang)。base 歸一成同一 node。 */ + aliases: z.array(z.string()).optional(), + /** 向量化打標:此 node 要不要進向量庫。預設 true。ingest 打標,base 讀標執行。 */ + embed: z.boolean().optional(), + entity_type: z.enum(['person', 'event', 'product', 'market', 'org']).optional(), + }) + .strict(); + +export const EnvelopeEdgeSchema = z + .object({ + subject: z.string().min(1), + predicate: z.string().min(1), + object: z.string().min(1), + /** 謂詞向量化打標(裸詞 embed,無描述)→ predicate_vector,支援關係過濾。預設 true。 */ + predicate_embed: z.boolean().optional(), + confidence: z.number().min(0).max(1).optional(), + }) + .strict(); + +export const EnvelopeSchema = z + .object({ + source: z + .object({ + /** 'github:/@',= 快照鍵 + get_source 指標。 */ + uri: z.string().min(1), + /** 來源檔內容 hash(快照鍵)。graph 比對同 hash → no-op。 */ + content_hash: z.string().min(1), + anchor: z.string().optional(), + commit: z.string().optional(), + block_id: z.string().optional(), + }) + .strict(), + extractor: z + .object({ + model: z.string().min(1), + tier: z.enum(['shallow', 'deep']), + extracted_at: z.number().int().optional(), + }) + .strict(), + nodes: z.array(EnvelopeNodeSchema).optional(), + triplets: z.array(EnvelopeEdgeSchema).min(1), + }) + .strict(); + +export type EnvelopeNode = z.infer; +export type EnvelopeEdge = z.infer; +export type Envelope = z.infer; + +/** graph 領域欄位 — ingest 絕不可送(送了被 graph 422)。用於本地自檢,提早攔。 */ +export const FORBIDDEN_TOP_KEYS = ['id', 'clusters', 'bridge_score', 'created_at', 'updated_at'] as const; +export const FORBIDDEN_EDGE_KEYS = ['subject_entity_type', 'object_entity_type'] as const; diff --git a/tests/envelope.test.ts b/tests/envelope.test.ts new file mode 100644 index 0000000..103110a --- /dev/null +++ b/tests/envelope.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { buildEnvelope } from '../src/lib/envelope'; + +const base = { + source: { uri: 'github:o/r@a.md', content_hash: 'abc' }, + extractor: { model: 'local-harvest', tier: 'shallow' as const }, + triplets: [{ subject: 'A', predicate: 'p', object: 'B', predicate_embed: true }], +}; + +describe('buildEnvelope', () => { + it('組合法 envelope(含向量化打標欄位)', () => { + const env = buildEnvelope({ + ...base, + nodes: [{ name: 'A', gloss: 'a', aliases: ['a2'], embed: true, id: 'A' }], + }); + expect(env.source.uri).toBe('github:o/r@a.md'); + expect(env.nodes?.[0].embed).toBe(true); + expect(env.nodes?.[0].id).toBe('A'); + expect(env.triplets[0].predicate_embed).toBe(true); + }); + + it('node 帶禁送欄位(bridge_score)→ strict throw(本地提早攔,不等 graph 422)', () => { + expect(() => buildEnvelope({ ...base, nodes: [{ name: 'A', embed: true }] })).not.toThrow(); + expect(() => + buildEnvelope({ ...base, nodes: [{ name: 'A', bridge_score: 0.5 } as any] }), + ).toThrow(); + }); + + it('node 帶 graph 領域 record id(非去重 id)以外的禁送鍵 → strict throw', () => { + // 契約允許 nodes[].id(去重鍵);但 clusters 是 graph 領域 → strict 擋。 + expect(() => buildEnvelope({ ...base, nodes: [{ name: 'A', id: 'A', embed: true }] })).not.toThrow(); + expect(() => buildEnvelope({ ...base, nodes: [{ name: 'A', clusters: ['c'] } as any] })).toThrow(); + }); + + it('禁送邊上 entity_type → strict throw', () => { + expect(() => + buildEnvelope({ + ...base, + triplets: [{ subject: 'A', predicate: 'p', object: 'B', subject_entity_type: 'person' } as any], + }), + ).toThrow(); + }); + + it('無 triplets → throw(契約 min 1)', () => { + expect(() => buildEnvelope({ ...base, triplets: [] })).toThrow(); + }); +}); diff --git a/tests/extract.test.ts b/tests/extract.test.ts new file mode 100644 index 0000000..586f382 --- /dev/null +++ b/tests/extract.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest'; +import { extract, parseExtractJson, type LlmCaller } from '../src/lib/extract'; + +const GOOD_JSON = JSON.stringify({ + nodes: [ + { name: '原子筆記', gloss: '一個不可再分論點的記錄單元' }, + { name: '傳統筆記', gloss: '多主題混雜的記錄' }, + ], + triplets: [{ subject: '原子筆記', predicate: '對立於', object: '傳統筆記', confidence: 0.9 }], +}); + +function caller(model: string, out: string | (() => Promise)): LlmCaller { + return { model, call: typeof out === 'string' ? async () => out : out }; +} + +describe('parseExtractJson', () => { + it('解析 fenced JSON + 打標 embed/predicate_embed', () => { + const g = parseExtractJson('```json\n' + GOOD_JSON + '\n```'); + expect(g.triplets[0].predicate_embed).toBe(true); + expect(g.nodes[0].embed).toBe(true); + expect(g.triplets[0].confidence).toBe(0.9); + }); + + it('無 triplets → throw', () => { + expect(() => parseExtractJson(JSON.stringify({ nodes: [], triplets: [] }))).toThrow(); + }); +}); + +describe('extract', () => { + it('淺萃成功不升級', async () => { + const r = await extract('原文', caller('haiku', GOOD_JSON)); + expect(r.tier).toBe('shallow'); + expect(r.escalated).toBe(false); + expect(r.model).toBe('haiku'); + }); + + it('淺萃 JSON-fail → 升 deep(升級閘)', async () => { + const r = await extract('原文', caller('haiku', 'not json at all'), caller('claude', GOOD_JSON)); + expect(r.escalated).toBe(true); + expect(r.tier).toBe('deep'); + expect(r.model).toBe('claude'); + expect(r.triplets.length).toBe(1); + }); + + it('淺萃失敗且無 deep caller → throw', async () => { + await expect(extract('原文', caller('haiku', 'garbage'))).rejects.toThrow(); + }); + + it('端點對齊護欄:模型吐對不齊端點 → 自動補進 nodes', async () => { + const skewed = JSON.stringify({ + nodes: [{ name: 'A' }], + triplets: [{ subject: 'A', predicate: '連到', object: 'B(沒在 nodes)' }], + }); + const r = await extract('原文', caller('haiku', skewed)); + // B 被自動補成 node → 端點全對齊 + expect(r.nodes.some((n) => n.name === 'B(沒在 nodes)')).toBe(true); + }); +}); diff --git a/tests/graph-client.test.ts b/tests/graph-client.test.ts new file mode 100644 index 0000000..115f42f --- /dev/null +++ b/tests/graph-client.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import { makeGraphClient } from '../src/lib/graph-client'; +import type { Envelope } from '../src/types'; + +const env: Envelope = { + source: { uri: 'github:o/r@a.md', content_hash: 'abc' }, + extractor: { model: 'local-harvest', tier: 'shallow' }, + triplets: [{ subject: 'A', predicate: 'p', object: 'B' }], +}; + +function mockFetch(status: number, body: unknown): typeof fetch { + return (async () => + new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' } })) as any; +} + +describe('makeGraphClient', () => { + it('GRAPH_BASE_URL 未設 → 誠實回 ok:false,不假綠、不打網路', async () => { + let called = false; + const client = makeGraphClient(undefined, undefined, (async () => { + called = true; + return new Response('{}'); + }) as any); + const r = await client.postEnvelope(env); + expect(r.ok).toBe(false); + expect(r.error).toContain('未設'); + expect(called).toBe(false); + }); + + it('200 → ok + 帶 graph 回的 {skipped,ingested,deprecated}', async () => { + const client = makeGraphClient('https://graph.example', 'tok', mockFetch(200, { skipped: false, ingested: 1, deprecated: 0 })); + const r = await client.postEnvelope(env); + expect(r.ok).toBe(true); + expect((r.body as any).ingested).toBe(1); + }); + + it('422 → ok:false 帶 issues(供修禁送欄位)', async () => { + const client = makeGraphClient('https://graph.example', undefined, mockFetch(422, { error: 'invalid envelope', issues: [{ path: ['bridge_score'] }] })); + const r = await client.postEnvelope(env); + expect(r.ok).toBe(false); + expect(r.status).toBe(422); + expect((r.body as any).issues).toBeDefined(); + }); +}); diff --git a/tests/harvest.test.ts b/tests/harvest.test.ts new file mode 100644 index 0000000..a9bc956 --- /dev/null +++ b/tests/harvest.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from 'vitest'; +import { harvestCard, parseEntities, parseEdges, parseFrontmatter } from '../src/lib/harvest'; + +const CARD = `--- +tags: [掛載架構, 架構設計] +gloss: ingest 在 KBDB 堆疊裡的位置。 +--- +# 掛載架構 + +← [[ingest/00-INDEX]] + +## 摘要 +KBDB 是三層堆疊。 + +## 實體 +- **kbdb-ingest-plugin**(餵食器) — 最薄一層,純 POST 候選。 +- **base KBDB**(arcrun/kbdb/基本盤) — 最底儲存層。 + +## 關聯 +### 內文知識關係 +- kbdb-ingest-plugin >> 掛載於 >> base KBDB +### 卡片關係 +- [[掛載架構]] >> 受約束於 >> [[envelope-契約]] +`; + +describe('parseFrontmatter', () => { + it('抽出 gloss', () => { + const { fm, body } = parseFrontmatter(CARD); + expect(fm.gloss).toBe('ingest 在 KBDB 堆疊裡的位置。'); + expect(body).toContain('# 掛載架構'); + }); +}); + +describe('parseEntities', () => { + it('解析正規名 + aliases + gloss', () => { + const { body } = parseFrontmatter(CARD); + const nodes = parseEntities(body); + expect(nodes.map((n) => n.name)).toEqual(['kbdb-ingest-plugin', 'base KBDB']); + expect(nodes[1].aliases).toEqual(['arcrun/kbdb', '基本盤']); + expect(nodes[0].gloss).toBe('最薄一層,純 POST 候選。'); + expect(nodes[0].embed).toBe(true); + }); +}); + +describe('parseEdges', () => { + it('解析 typed-edge、去 [[ ]]、標記卡對卡', () => { + const { body } = parseFrontmatter(CARD); + const edges = parseEdges(body); + expect(edges).toContainEqual({ subject: 'kbdb-ingest-plugin', predicate: '掛載於', object: 'base KBDB', predicate_embed: true, subjectIsCard: false, objectIsCard: false }); + expect(edges).toContainEqual({ subject: '掛載架構', predicate: '受約束於', object: 'envelope-契約', predicate_embed: true, subjectIsCard: true, objectIsCard: true }); + }); +}); + +describe('harvestCard', () => { + it('卡標題 node 帶 frontmatter gloss、含內文 node', () => { + const r = harvestCard(CARD); + const titleNode = r.nodes.find((n) => n.name === '掛載架構'); + expect(titleNode?.gloss).toBe('ingest 在 KBDB 堆疊裡的位置。'); + expect(r.nodes.some((n) => n.name === 'base KBDB')).toBe(true); + expect(r.triplets.length).toBe(2); + }); + + it('內文端點對齊(無對不齊)', () => { + const r = harvestCard(CARD); + // kbdb-ingest-plugin / base KBDB 都在 ## 實體;卡對卡端點不要求 + expect(r.unalignedEndpoints).toEqual([]); + }); +}); diff --git a/tests/source-pipeline.test.ts b/tests/source-pipeline.test.ts new file mode 100644 index 0000000..1530880 --- /dev/null +++ b/tests/source-pipeline.test.ts @@ -0,0 +1,73 @@ +import { describe, it, expect } from 'vitest'; +import { makeSourceUri, parseSourceUri, contentHash, pullRepoMarkdown, type GitHubFetcher } from '../src/lib/source-adapter'; +import { processSource } from '../src/lib/pipeline'; +import type { LlmCaller } from '../src/lib/extract'; + +describe('source-adapter uri', () => { + it('makeSourceUri / parseSourceUri round-trip', () => { + const uri = makeSourceUri('uncle6me-web', 'kbdb-ingest-plugin', 'system-dev/wiki/cards/ingest/掛載架構.md'); + expect(uri).toBe('github:uncle6me-web/kbdb-ingest-plugin@system-dev/wiki/cards/ingest/掛載架構.md'); + expect(parseSourceUri(uri)).toEqual({ + owner: 'uncle6me-web', + repo: 'kbdb-ingest-plugin', + path: 'system-dev/wiki/cards/ingest/掛載架構.md', + }); + }); + + it('content-hash 穩定且隨內容變', async () => { + const a = await contentHash('hello'); + expect(a).toBe(await contentHash('hello')); + expect(a).not.toBe(await contentHash('world')); + }); +}); + +const HARVEST_CARD = `--- +gloss: 卡標題定義。 +--- +# 卡A +## 實體 +- **甲** — 甲的定義。 +- **乙** — 乙的定義。 +## 關聯 +- 甲 >> 連到 >> 乙 +`; + +function mockFetcher(files: Record): GitHubFetcher { + return { + async listMarkdown() { + return Object.keys(files); + }, + async getFile(_o, _r, path) { + return { text: files[path], commit: 'sha1' }; + }, + }; +} + +describe('pullRepoMarkdown + processSource', () => { + it('採取路徑 A:拉檔 → harvest → envelope(不 extract)', async () => { + const sources = await pullRepoMarkdown(mockFetcher({ 'cards/a.md': HARVEST_CARD }), 'o', 'r'); + expect(sources.length).toBe(1); + const result = await processSource(sources[0]); + expect(result.path).toBe('harvest'); + expect(result.envelope?.triplets).toEqual([{ subject: '甲', predicate: '連到', object: '乙', predicate_embed: true }]); + expect(result.envelope?.extractor.model).toBe('local-harvest'); + }); + + it('採不到三元組 + 無萃取模型 → skipped(不假萃)', async () => { + const sources = await pullRepoMarkdown(mockFetcher({ 'plain.md': '# 純文字\n沒有三元組。' }), 'o', 'r'); + const result = await processSource(sources[0]); + expect(result.path).toBe('skipped'); + expect(result.envelope).toBeNull(); + }); + + it('採不到 → fallback extract(路徑 B)', async () => { + const caller: LlmCaller = { + model: 'haiku', + call: async () => JSON.stringify({ nodes: [{ name: '甲' }], triplets: [{ subject: '甲', predicate: '是', object: '乙' }] }), + }; + const sources = await pullRepoMarkdown(mockFetcher({ 'plain.md': '# 純文字\n甲是乙。' }), 'o', 'r'); + const result = await processSource(sources[0], { shallowCaller: caller }); + expect(result.path).toBe('extract'); + expect(result.envelope?.extractor.model).toBe('haiku'); + }); +}); diff --git a/tests/weave.test.ts b/tests/weave.test.ts new file mode 100644 index 0000000..49cc978 --- /dev/null +++ b/tests/weave.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest'; +import { weave, flattenForPost, type RepoEnvelopes } from '../src/lib/weave'; +import type { Envelope } from '../src/types'; + +function env(uri: string, nodes: string[], triplets: Array<[string, string, string]>): Envelope { + return { + source: { uri, content_hash: uri }, + extractor: { model: 'local-harvest', tier: 'shallow' }, + nodes: nodes.map((n) => ({ name: n, embed: true })), + triplets: triplets.map(([s, p, o]) => ({ subject: s, predicate: p, object: o })), + }; +} + +const repos: RepoEnvelopes[] = [ + { repo: 'o/repoA', envelopes: [env('github:o/repoA@x.md', ['Arcrun', '餵食器'], [['Arcrun', '包含', '餵食器']])] }, + { repo: 'o/repoB', envelopes: [env('github:o/repoB@y.md', ['Arcrun', '圖層'], [['Arcrun', '依賴', '圖層']])] }, +]; + +describe('weave', () => { + it('偵測跨庫橋(同名節點跨 ≥2 repo)', () => { + const r = weave(repos); + const bridge = r.bridges.find((b) => b.node === 'Arcrun'); + expect(bridge?.repos).toEqual(['o/repoA', 'o/repoB']); + expect(r.totalTriplets).toBe(2); + }); + + it('偵測跨庫異見(同 s/o 對、不同謂詞跨 repo)', () => { + const diverge: RepoEnvelopes[] = [ + { repo: 'o/repoA', envelopes: [env('github:o/repoA@x.md', ['X', 'Y'], [['X', '支持', 'Y']])] }, + { repo: 'o/repoB', envelopes: [env('github:o/repoB@y.md', ['X', 'Y'], [['X', '反對', 'Y']])] }, + ]; + const r = weave(diverge); + expect(r.divergences.length).toBe(1); + expect(r.divergences[0].predicatesByRepo.map((p) => p.predicate).sort()).toEqual(['反對', '支持']); + }); + + it('flattenForPost 攤平所有 envelope(順序穩定)', () => { + expect(flattenForPost(repos).length).toBe(2); + }); + + it('ingest 不算 bridge_score(橋只標 repos,無分數欄位)', () => { + const r = weave(repos); + expect(r.bridges[0]).not.toHaveProperty('bridge_score'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e2926b0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "src", + "types": ["@cloudflare/workers-types"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..91fd582 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +// ingest 純餵食器:不綁 D1/Vectorize/AI。測試走純 node + mock(fetch / graph client)。 +export default defineConfig({ + test: { + environment: 'node', + include: ['tests/**/*.test.ts'], + }, +}); diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..cf8f155 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,24 @@ +name = "kbdb-ingest-plugin" +main = "src/index.ts" +compatibility_date = "2025-02-19" +compatibility_flags = ["nodejs_compat"] +workers_dev = true + +# KBDB-ingest 插件 = 純餵食器:GitHub 拉 + 採取/萃取 + 跨庫織網 → POST envelope 給 graph。 +# 鐵律:不碰儲存(無 D1/Vectorize/AI 綁定——那些屬 base/graph,ingest 不直連)。 +# 部署走 wrangler,繞 GitHub Actions(被 flag 教訓)。 + +[vars] +ENVIRONMENT = "development" +# graph 插件寫入端 base URL(POST {GRAPH_BASE_URL}/triplets/ingest)。 +# 部署前用 `wrangler secret put` 或在此填,例如 https://kbdb-graph..workers.dev +GRAPH_BASE_URL = "" +# 萃取(路徑 B)預設模型意圖。"shallow"=Haiku/Workers AI;"deep"=Claude via CC。 +DEFAULT_EXTRACT_TIER = "shallow" + +[alias] +"zod/v3" = "zod" +"zod/v4" = "zod" +"zod/v4-mini" = "zod" + +# GITHUB_TOKEN / GRAPH_INTERNAL_TOKEN / ANTHROPIC 等機敏值走 `wrangler secret put`,不寫這裡。