diff --git a/.gitignore b/.gitignore index 0d974fd..82ac3ad 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ env/secrets.json .cursor/ .codex/ .claude/ +.plan-reviews/ node_modules/ # VitePress build output (generated by CI) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a12bfa..86e8340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,10 @@ All notable changes to ai-coding-kit will be documented in this file. --- -## [3.0.0] — 2026-07-05 +## [3.0.0] — 2026-07-06 + +### Removed +- **rag-gateway**: 移除 Universal RAG Gateway 模块,由 skills-engineering/plan-reviews 中更轻量的嵌入式知识库方案替代 ### Added - **i18n 分层**: SKILL.md 英文元指令 + en-US 治理层镜像(rule_index / cognitive_adversary_mode / self_evolution),IR-001 从"强制中文"改为"语言匹配用户输入" diff --git a/README.md b/README.md index 7ced368..2f60e46 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,15 @@ [![Agent Skills](https://img.shields.io/badge/Agent%20Skills-8%2B%20AI%20Coding%20Tools-5856D6)](skills-engineering/README.md) [![iOS Engineer Skill](https://img.shields.io/badge/iOS%20Engineer-Swift%20%7C%20SwiftUI%20%7C%20UIKit-0A84FF)](skills-engineering/ios-engineer/SKILL.md) [![MCP Config Sync](https://img.shields.io/badge/MCP%20Config-8%20Platforms-663399)](sync/README.md) -[![Universal RAG Gateway](https://img.shields.io/badge/Universal%20RAG%20Gateway-TypeScript%20%7C%20Fastify-34C759)](rag-gateway/README.md) [![Validate Skills](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml/badge.svg?branch=feature_3.0.0)](https://github.com/i-stack/ai-coding-kit/actions/workflows/validate.yml) [![Check Hardcoded Paths](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml/badge.svg)](https://github.com/i-stack/ai-coding-kit/actions/workflows/hardcoded-paths.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -> **One kit. All your AI coding tools.** Agent Skills management, MCP configuration sync, iOS engineering rules, and a Universal RAG Gateway — unified for Cursor, CodeBuddy, Codex, Claude Code, Gemini CLI, Continue, Cline, and Xcode Coding Assistant. +> **One kit. All your AI coding tools.** Agent Skills management, MCP configuration sync, and iOS engineering rules — unified for Cursor, CodeBuddy, Codex, Claude Code, Gemini CLI, Continue, Cline, and Xcode Coding Assistant. **ai-coding-kit** is a local-first AI coding workflow toolkit. Define your MCP servers, API keys, Agent Skills, and platform settings once — auto-sync to every AI coding host you use. -面向 AI Coding / Agentic Coding / MCP(模型上下文协议)的多工具本地工程化工具包。为同时使用多个 AI 编码工具的开发者提供统一的 Agent Skill 维护、MCP 配置同步、iOS 工程规则和智能网关路由。 +面向 AI Coding / Agentic Coding / MCP(模型上下文协议)的多工具本地工程化工具包。为同时使用多个 AI 编码工具的开发者提供统一的 Agent Skill 维护、MCP 配置同步、iOS 工程规则和本地嵌入式知识库。 ## Quick Start @@ -43,7 +42,7 @@ bash sync.sh | **skills-engineering/** | Agent Skill 内容源、多端同步、受控演进 | [README](skills-engineering/README.md) | | **sync/** | MCP 配置同步引擎,注入 secrets 渲染到各平台原生格式 | [README](sync/README.md) | | **env/** | 配置数据源(secrets + MCP 定义 + 平台配置) | [README](env/README.md) | -| **rag-gateway/** | TypeScript / Fastify 通用 RAG 网关(OpenAI 兼容 API) | [README](rag-gateway/README.md) | +| **skills-engineering/plan-reviews/** | Plan 知识库 —— 自动索引 plan-grill/cross-model-review 产物,语义搜索 + 实体图谱 | [README](skills-engineering/plan-reviews/README.md) | | **hooks/** | 项目钩子脚本(xmcp 初始化等) | [README](hooks/README.md) | | **.githooks/** | Git 提交/推送守卫(pre-commit + pre-push) | [README](.githooks/README.md) | @@ -76,7 +75,7 @@ bash install-hooks.sh - **Developers using multiple AI coding tools** — define MCP servers and Agent Skills once, sync everywhere. - **iOS / Swift engineers** — production-grade AI coding rules for Swift, SwiftUI, UIKit, concurrency, testing, and migration. -- **AI infrastructure builders** — local memory, semantic retrieval, declarative tools, and OpenAI-compatible RAG gateway patterns. +- **AI infrastructure builders** — local embedded knowledge base with semantic search and entity graph, zero external services. - **Team maintainers** — single source of truth for MCP configuration, API keys, and model settings. ## License diff --git a/docs/index.md b/docs/index.md index 2dca747..3a61dfc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ title: ai-coding-kit hero: name: ai-coding-kit text: One Kit. All AI Coding Tools. - tagline: Agent Skills management, MCP configuration sync, iOS engineering rules, and Universal RAG Gateway — unified for 8+ AI coding platforms. + tagline: Agent Skills management, MCP configuration sync, and iOS engineering rules — unified for 8+ AI coding platforms. image: false actions: - theme: brand @@ -24,9 +24,6 @@ features: - icon: 🍎 title: iOS Engineering Rules details: Production-grade Swift / SwiftUI / UIKit rules with 40+ rule IDs, symptom routing, task triage, and auto-evolution — maintained by an Agent Skill system. - - icon: 🌐 - title: Universal RAG Gateway - details: TypeScript / Fastify RAG gateway with OpenAI-compatible API — local memory, semantic retrieval, and multi-provider routing. - icon: 🔒 title: Global Engineering Discipline details: Six global skills spanning security compliance, epistemic integrity, logical reasoning, cognitive expansion, and problem analysis — apply to any platform. @@ -80,6 +77,5 @@ npm install -g @i-stack/ai-coding-kit | **skills-engineering/** | Agent Skill content, multi-platform sync, governed evolution | | **sync/** | MCP config sync engine — injects secrets, renders to native formats | | **env/** | Config data source (secrets + MCP definitions + platform configs) | -| **rag-gateway/** | TypeScript / Fastify Universal RAG Gateway (OpenAI-compatible API) | | **hooks/** | Project hooks (xmcp init, etc.) | | **.githooks/** | Git commit/push guards (pre-commit + pre-push) | diff --git a/env/README.md b/env/README.md index 01641f2..c754571 100644 --- a/env/README.md +++ b/env/README.md @@ -26,7 +26,6 @@ env/ │ ├── codebuddy.json │ ├── continue.json │ ├── gemini.json -│ └── rag-gateway.json │ └── templates/ ← 新增 MCP/平台的参考模板 ├── mcp.template.json diff --git a/env/platforms/rag-gateway.json b/env/platforms/rag-gateway.json deleted file mode 100644 index bf819e7..0000000 --- a/env/platforms/rag-gateway.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "env": { - "EMBEDDING_API_KEY": "${rag-gateway.key}", - "EMBEDDING_BASE_URL": "${rag-gateway.url}", - "EMBEDDING_MODEL": "bge-m3" - } -} diff --git a/env/secrets.json.example b/env/secrets.json.example index 3ee5df9..6fee503 100644 --- a/env/secrets.json.example +++ b/env/secrets.json.example @@ -25,9 +25,5 @@ "gemini": { "url": "https://your-gemini-proxy.example.com", "key": "sk-your-gemini-api-key" - }, - "rag-gateway": { - "url": "https://your-embedding-provider.example.com/v1", - "key": "sk-your-embedding-api-key" } } diff --git a/package.json b/package.json index 315fe69..5617ebe 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@i-stack/ai-coding-kit", "version": "3.0.0", "type": "module", - "description": "One kit for all AI coding tools — Agent Skills management, MCP configuration sync, iOS engineering rules, and Universal RAG Gateway", + "description": "One kit for all AI coding tools — Agent Skills management, MCP configuration sync, and iOS engineering rules", "homepage": "https://github.com/i-stack/ai-coding-kit#readme", "repository": { "type": "git", @@ -20,7 +20,6 @@ "skills-engineering/", "sync/", "env/", - "rag-gateway/", "hooks/", ".githooks/", "sync.sh", @@ -37,7 +36,6 @@ "model-context-protocol", "ios", "swift", - "rag", "claude-code", "codex", "cursor", diff --git a/rag-gateway/.env.example b/rag-gateway/.env.example deleted file mode 100644 index 2f4dde2..0000000 --- a/rag-gateway/.env.example +++ /dev/null @@ -1,34 +0,0 @@ -# Gateway server -GATEWAY_PORT=3000 -GATEWAY_HOST=0.0.0.0 - -# ── Embedding Service (required for Qdrant semantic search) ──────────────────── -# These keys are AUTO-SOURCED from env/secrets.json at the repo root (platforms.rag-gateway.env). -# Edit that file (copy env/secrets.json.example first), then restart the gateway. -# -# secrets.json key → process.env key (identity, no transform) -# EMBEDDING_API_KEY → EMBEDDING_API_KEY -# EMBEDDING_BASE_URL → EMBEDDING_BASE_URL -# EMBEDDING_MODEL → EMBEDDING_MODEL -# -# Values set in THIS .env file take precedence over secrets.json. -# Uncomment and set any of the below to override for local testing: -# -#EMBEDDING_API_KEY=sk-xxx -#EMBEDDING_BASE_URL=https://api.openai.com/v1 -#EMBEDDING_MODEL=bge-m3 - -# ── Entity Extraction LLM (optional for GraphRAG) ──────────────────────────── -# Falls back to EMBEDDING_API_KEY / EMBEDDING_BASE_URL if unset. -#EXTRACTION_API_KEY= -#EXTRACTION_BASE_URL= -#EXTRACTION_MODEL=gpt-4o-mini - -# PostgreSQL (required for GraphRAG entity store; optional for message transcripts) -DATABASE_URL=postgresql://localhost:5432/rag_gateway - -# Qdrant semantic memory -QDRANT_URL=http://localhost:6333 - -# GraphRAG entity-relationship memory -GRAPH_RAG_ENABLED=true \ No newline at end of file diff --git a/rag-gateway/.gitignore b/rag-gateway/.gitignore deleted file mode 100644 index 61d9155..0000000 --- a/rag-gateway/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -dist/ -.env -.bin/ \ No newline at end of file diff --git a/rag-gateway/README.md b/rag-gateway/README.md deleted file mode 100644 index 16ef7e9..0000000 --- a/rag-gateway/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# rag-gateway - -基于 TypeScript / Fastify 的通用 RAG(检索增强生成)网关,提供 OpenAI 兼容 API,可作为本地 RAG 服务的自托管替代方案。 - -## 核心能力 - -- **OpenAI 兼容 API** — 标准 `/v1/chat/completions` 接口,可接入任何 OpenAI 客户端 -- **语义记忆** — 基于 Qdrant 向量数据库的持久化对话记忆检索 -- **GraphRAG** — 实体关系图,从对话中提取结构化知识 -- **声明式工具系统** — 通过 `tools.json` 定义工具,无需编写代码即可扩展 -- **MCP 接口** — 通过 SSE 暴露 `tools/list` 和 `tools/call` - -## 快速开始 - -```bash -cd rag-gateway -cp .env.example .env -# 编辑 .env 填写 embedding API key / Qdrant URL 等 -$EDITOR .env -npm install -npm run dev -``` - -## 目录结构 - -```text -rag-gateway/ -├── src/ -│ ├── index.ts ← Fastify 服务器入口 -│ ├── config.ts ← 配置加载(.env + rag-gateway.json) -│ ├── db/ ← PostgreSQL 持久化(转录、图谱) -│ ├── vector/ ← Embedding + Qdrant 向量存储 -│ ├── entity/ ← 实体关系提取 -│ ├── tool/ ← 声明式工具注册与执行 -│ ├── mcp/ ← MCP 服务器与客户端 -│ └── metrics.ts ← 指标收集 -├── tests/ ← Vitest 单元/集成测试 -├── scripts/ ← 调试和数据填充脚本 -├── tools.json ← 声明式工具定义 -└── .env.example ← 环境变量模板 -``` - -## 环境变量 - -| 变量 | 说明 | -|------|------| -| `GATEWAY_PORT` | 服务端口(默认 3000) | -| `EMBEDDING_API_KEY` | Embedding 服务 API Key | -| `EMBEDDING_BASE_URL` | Embedding 服务地址 | -| `EMBEDDING_MODEL` | Embedding 模型名称 | -| `EXTRACTION_LLM_API_KEY` | 实体提取用 LLM Key | -| `POSTGRES_URL` | PostgreSQL 连接串(可选) | -| `QDRANT_URL` | Qdrant 向量库地址(可选) | -| `GRAPHRAG_ENABLED` | 是否启用 GraphRAG(`true`/`false`) | - -配置也可从 `env/platforms/rag-gateway.json` 自动加载默认值(`.env` 优先)。 diff --git a/rag-gateway/package-lock.json b/rag-gateway/package-lock.json deleted file mode 100644 index 79269cc..0000000 --- a/rag-gateway/package-lock.json +++ /dev/null @@ -1,4149 +0,0 @@ -{ - "name": "universal-rag-gateway", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "universal-rag-gateway", - "version": "0.1.0", - "dependencies": { - "@anthropic-ai/sdk": "^0.104.1", - "@fastify/cors": "^11.0.1", - "@modelcontextprotocol/sdk": "^1.29.0", - "dotenv": "^16.4.7", - "fastify": "^5.2.1", - "openai": "^4.86.2", - "pg": "^8.21.0" - }, - "devDependencies": { - "@types/node": "^22.13.0", - "@types/pg": "^8.20.0", - "tsx": "^4.19.3", - "typescript": "^5.7.3", - "vitest": "^4.1.8" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.104.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.104.1.tgz", - "integrity": "sha512-gGACa/+IaiXzRRmF96aOhamoBgapKRBiFWbmmTFP8aMkpaEcuStF+Q61bjo4vPxBM7gqWJNZqsngslRdnLHv0Q==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1", - "standardwebhooks": "^1.0.0" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@fastify/ajv-compiler": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", - "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0" - } - }, - "node_modules/@fastify/cors": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.2.0.tgz", - "integrity": "sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fastify-plugin": "^5.0.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/@fastify/error": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", - "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/fast-json-stringify-compiler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", - "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "fast-json-stringify": "^6.0.0" - } - }, - "node_modules/@fastify/forwarded": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", - "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/@fastify/merge-json-schemas": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", - "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/@fastify/proxy-addr": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", - "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/forwarded": "^3.0.0", - "ipaddr.js": "^2.1.0" - } - }, - "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, - "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/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", - "license": "MIT", - "dependencies": { - "@hono/node-server": "^1.19.9", - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", - "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@stablelib/base64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", - "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.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/@types/node": { - "version": "22.19.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.21.tgz", - "integrity": "sha512-VMeFBSCKQKmm2swI2kW51SFusDqekC6q9trBCvJ/JliDchFSuoYYKN7yVNjPthP1HKZcx3U1gI/wTcEBjEFKTA==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.8", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "@vitest/utils": "4.1.8", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.8", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "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/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/avvio": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", - "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/error": "^4.0.0", - "fastq": "^1.17.1" - } - }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "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/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "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/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.2.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-json-stringify": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.4.0.tgz", - "integrity": "sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/merge-json-schemas": "^0.2.0", - "ajv": "^8.12.0", - "ajv-formats": "^3.0.1", - "fast-uri": "^3.0.0", - "json-schema-ref-resolver": "^3.0.0", - "rfdc": "^1.2.0" - } - }, - "node_modules/fast-querystring": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", - "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", - "license": "MIT", - "dependencies": { - "fast-decode-uri-component": "^1.0.1" - } - }, - "node_modules/fast-sha256": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", - "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", - "license": "Unlicense" - }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastify": { - "version": "5.8.5", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", - "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "@fastify/ajv-compiler": "^4.0.5", - "@fastify/error": "^4.0.0", - "@fastify/fast-json-stringify-compiler": "^5.0.0", - "@fastify/proxy-addr": "^5.0.0", - "abstract-logging": "^2.0.1", - "avvio": "^9.0.0", - "fast-json-stringify": "^6.0.0", - "find-my-way": "^9.0.0", - "light-my-request": "^6.0.0", - "pino": "^9.14.0 || ^10.1.0", - "process-warning": "^5.0.0", - "rfdc": "^1.3.1", - "secure-json-parse": "^4.0.0", - "semver": "^7.6.0", - "toad-cache": "^3.7.0" - } - }, - "node_modules/fastify-plugin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", - "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "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/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-my-way": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", - "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-querystring": "^1.0.0", - "safe-regex2": "^5.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hono": { - "version": "4.12.25", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", - "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/json-schema-ref-resolver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", - "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json-schema-typed": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" - }, - "node_modules/light-my-request": { - "version": "6.6.0", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", - "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "dependencies": { - "cookie": "^1.0.1", - "process-warning": "^4.0.0", - "set-cookie-parser": "^2.6.0" - } - }, - "node_modules/light-my-request/node_modules/process-warning": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", - "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", - "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/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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/pg": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", - "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.13.0", - "pg-pool": "^3.14.0", - "pg-protocol": "^1.14.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.4.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", - "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", - "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", - "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", - "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, - "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/pino": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^4.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", - "license": "MIT" - }, - "node_modules/pkce-challenge": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", - "license": "MIT", - "engines": { - "node": ">=16.20.0" - } - }, - "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/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", - "license": "MIT" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "license": "MIT", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ret": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", - "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rfdc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", - "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", - "license": "MIT" - }, - "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.133.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.3", - "@rolldown/binding-darwin-arm64": "1.0.3", - "@rolldown/binding-darwin-x64": "1.0.3", - "@rolldown/binding-freebsd-x64": "1.0.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", - "@rolldown/binding-linux-arm64-gnu": "1.0.3", - "@rolldown/binding-linux-arm64-musl": "1.0.3", - "@rolldown/binding-linux-ppc64-gnu": "1.0.3", - "@rolldown/binding-linux-s390x-gnu": "1.0.3", - "@rolldown/binding-linux-x64-gnu": "1.0.3", - "@rolldown/binding-linux-x64-musl": "1.0.3", - "@rolldown/binding-openharmony-arm64": "1.0.3", - "@rolldown/binding-wasm32-wasi": "1.0.3", - "@rolldown/binding-win32-arm64-msvc": "1.0.3", - "@rolldown/binding-win32-x64-msvc": "1.0.3" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/safe-regex2": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", - "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT", - "dependencies": { - "ret": "~0.5.0" - }, - "bin": { - "safe-regex2": "bin/safe-regex2.js" - } - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/secure-json-parse": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", - "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", - "license": "MIT" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", - "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4", - "side-channel-list": "^1.0.1", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "license": "MIT", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "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/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, - "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/standardwebhooks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", - "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", - "license": "MIT", - "dependencies": { - "@stablelib/base64": "^1.0.0", - "fast-sha256": "^1.3.0" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/thread-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", - "license": "MIT", - "dependencies": { - "real-require": "^1.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/thread-stream/node_modules/real-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", - "license": "MIT" - }, - "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": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/toad-cache": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.1.tgz", - "integrity": "sha512-5DXWzE4Vz7xNHsv+xQ+MGfJYyC78Aok3tEr0MNwHoRf7vZnga1mQXZ4/Nsodld4VR6Wd+VhfmqnNrsRJyYPfrQ==", - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-algebra": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", - "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", - "license": "MIT" - }, - "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/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.28.0" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", - "license": "MIT", - "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "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-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" - }, - "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", - "@vitejs/devtools": "^0.1.18", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.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 - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "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/vitest": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.8", - "@vitest/mocker": "4.1.8", - "@vitest/pretty-format": "4.1.8", - "@vitest/runner": "4.1.8", - "@vitest/snapshot": "4.1.8", - "@vitest/spy": "4.1.8", - "@vitest/utils": "4.1.8", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.8", - "@vitest/browser-preview": "4.1.8", - "@vitest/browser-webdriverio": "4.1.8", - "@vitest/coverage-istanbul": "4.1.8", - "@vitest/coverage-v8": "4.1.8", - "@vitest/ui": "4.1.8", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "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/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/rag-gateway/package.json b/rag-gateway/package.json deleted file mode 100644 index b92cd80..0000000 --- a/rag-gateway/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "universal-rag-gateway", - "version": "0.1.0", - "description": "Universal RAG Gateway — MVP", - "type": "module", - "scripts": { - "dev": "tsx watch src/index.ts", - "start": "node dist/index.js", - "build": "tsc", - "typecheck": "tsc --noEmit", - "test": "vitest run", - "test:watch": "vitest", - "test:unit": "vitest run tests/unit", - "test:integration": "vitest run tests/integration", - "test:e2e": "vitest run tests/e2e" - }, - "dependencies": { - "@anthropic-ai/sdk": "^0.104.1", - "@fastify/cors": "^11.0.1", - "@modelcontextprotocol/sdk": "^1.29.0", - "dotenv": "^16.4.7", - "fastify": "^5.2.1", - "openai": "^4.86.2", - "pg": "^8.21.0" - }, - "devDependencies": { - "@types/node": "^22.13.0", - "@types/pg": "^8.20.0", - "tsx": "^4.19.3", - "typescript": "^5.7.3", - "vitest": "^4.1.8" - } -} diff --git a/rag-gateway/scripts/debug.ts b/rag-gateway/scripts/debug.ts deleted file mode 100644 index 85ac49e..0000000 --- a/rag-gateway/scripts/debug.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { loadConfig } from "../src/config.js"; -import { EmbeddingService } from "../src/vector/embedding.js"; -import { QdrantStore } from "../src/vector/qdrant.js"; - -async function debug() { - const config = loadConfig(); - console.log("Config:", JSON.stringify(config, null, 2)); - - // Test embedding - const embedding = new EmbeddingService(config); - try { - const vec = await embedding.embed("test query RAG gateway"); - console.log("Embedding OK, vector length:", vec.length, "first 5:", vec.slice(0, 5)); - } catch (e) { - console.error("Embedding FAILED:", (e as Error).message); - } - - // Test Qdrant direct search - const qdrant = new QdrantStore(config.qdrantUrl!, { vectorSize: config.vectorSize }); - try { - const vec = await embedding.embed("RAG gateway"); - const results = await qdrant.search(vec, { limit: 5, tenantId: "default" }); - console.log("Direct Qdrant search results:", results.length); - for (const r of results) { - console.log(" score:", r.score, "text:", r.payload.text?.substring(0, 100)); - } - } catch (e) { - console.error("Qdrant search FAILED:", (e as Error).message); - } -} - -debug(); \ No newline at end of file diff --git a/rag-gateway/scripts/seed.ts b/rag-gateway/scripts/seed.ts deleted file mode 100644 index a8aa3cb..0000000 --- a/rag-gateway/scripts/seed.ts +++ /dev/null @@ -1,121 +0,0 @@ -import crypto from "node:crypto"; -import { loadConfig } from "../src/config.js"; -import { EmbeddingService } from "../src/vector/embedding.js"; -import { QdrantStore } from "../src/vector/qdrant.js"; -import { VectorStore } from "../src/vector/store.js"; -import { initDb } from "../src/db/index.js"; -import * as graphDb from "../src/db/graph.js"; - -async function seed() { - const config = loadConfig(); - if (!config.qdrantUrl) { - console.error("QDRANT_URL not set"); - process.exit(1); - } - - // ── PostgreSQL init (for GraphRAG) ─────────────────────────────────── - if (config.databaseUrl) { - initDb(config.databaseUrl); - console.log("PostgreSQL pool initialized"); - } else { - console.warn("DATABASE_URL not set — skipping GraphRAG entity seeding"); - } - - const embedding = new EmbeddingService(config); - const qdrant = new QdrantStore(config.qdrantUrl, { vectorSize: config.vectorSize }); - await qdrant.ensureCollection(); - await qdrant.ensurePayloadIndexes(); - const store = new VectorStore(embedding, qdrant); - - // ── Qdrant semantic documents ──────────────────────────────────────── - const docs = [ - "The RAG gateway is an MCP server that provides semantic memory search and entity graph retrieval.", - "Qdrant is used as the vector store for semantic memory in the RAG gateway.", - "The gateway connects to Claude via MCP SSE transport at /mcp/sse.", - "GraphRAG stores entities and relationships in PostgreSQL for knowledge graph search.", - "Tools are defined declaratively in tools.json and executed by ToolExecutorEngine.", - "The embedding service converts text to vectors using the bge-m3 model.", - "Entity extraction uses an LLM to extract structured entities from conversation text.", - "The gateway supports multi-tenant isolation via tenant_id and project_id parameters.", - "MCP clients discover available tools via tools/list and invoke them via tools/call.", - ]; - - for (const text of docs) { - await store.indexMessage({ - id: crypto.randomUUID(), - text, - kind: "system_prompt", - tenantId: "default", - sourceMessageId: crypto.randomUUID(), - conversationId: "seed-init", - }); - } - console.log(`Seeded ${docs.length} documents into Qdrant`); - - // ── GraphRAG entities and edges ────────────────────────────────────── - if (config.databaseUrl) { - const tenantId = "default"; - - // Define entities: [name, type, properties] - const entityDefs: Array<{ name: string; type: string; properties: Record }> = [ - { name: "MyApp", type: "project", properties: {} }, - { name: "PostgreSQL", type: "database", properties: { type: "Relational Database", purpose: "Primary data storage" } }, - { name: "Redis", type: "database", properties: { type: "In-memory data store", purpose: "Caching and session management" } }, - { name: "AuthService API", type: "api", properties: { purpose: "Handles authentication and token validation" } }, - { name: "EC2", type: "technology", properties: {} }, - { name: "Kafka", type: "technology", properties: {} }, - { name: "LogService", type: "service", properties: { description: "Centralized logging service" } }, - ]; - - // In-memory name → generated UUID mapping for edge construction - const nameToId = new Map(); - for (const def of entityDefs) { - const id = crypto.randomUUID(); - nameToId.set(def.name, id); - } - - // Upsert entities - const entities = entityDefs.map((def) => ({ - id: nameToId.get(def.name)!, - tenantId, - type: def.type, - name: def.name, - properties: def.properties, - })); - const persistedIds = await graphDb.upsertEntities(entities); - console.log(`Seeded ${persistedIds.size} entities into PostgreSQL`); - - // Define edges: [from, to, relation] - const edgeDefs: Array<{ from: string; to: string; relation: string }> = [ - { from: "MyApp", to: "PostgreSQL", relation: "uses" }, - { from: "MyApp", to: "Redis", relation: "uses" }, - { from: "MyApp", to: "AuthService API", relation: "uses" }, - ]; - - // Resolve actual DB IDs (ON CONFLICT may have returned different IDs than in-memory) - const resolvedNameToId = new Map(nameToId); - for (const [name, id] of persistedIds) { - resolvedNameToId.set(name, id); - } - - const edges = edgeDefs - .filter((e) => resolvedNameToId.has(e.from) && resolvedNameToId.has(e.to)) - .map((e) => ({ - id: crypto.randomUUID(), - tenantId, - fromEntityId: resolvedNameToId.get(e.from)!, - toEntityId: resolvedNameToId.get(e.to)!, - relation: e.relation, - properties: {}, - })); - - if (edges.length > 0) { - await graphDb.insertEdges(edges); - console.log(`Seeded ${edges.length} edges into PostgreSQL`); - } - - console.log(`GraphRAG seed complete — ${entityDefs.length} entities, ${edgeDefs.length} edges`); - } -} - -seed().catch(console.error); \ No newline at end of file diff --git a/rag-gateway/scripts/test-graphrag.ts b/rag-gateway/scripts/test-graphrag.ts deleted file mode 100644 index de5abad..0000000 --- a/rag-gateway/scripts/test-graphrag.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * MCP GraphRAG test script — connects to the running gateway via MCP SSE, - * calls search_memory, and checks whether GraphRAG entity search results appear. - * - * Run: npx tsx scripts/test-graphrag.ts - */ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; - -const GATEWAY_URL = "http://localhost:3000"; - -async function main() { - console.log(`\n🔌 Connecting to MCP gateway at ${GATEWAY_URL}/mcp/sse...`); - - const transport = new SSEClientTransport(new URL(`${GATEWAY_URL}/mcp/sse`)); - const client = new Client( - { name: "graphrag-test", version: "0.1.0" }, - { capabilities: {} }, - ); - - await client.connect(transport); - console.log("✅ Connected!"); - - // Step 1: list tools - const toolsResult = await client.listTools(); - const toolNames = toolsResult.tools.map((t: { name: string }) => t.name); - console.log(`📋 Available tools: ${toolNames.join(", ")}`); - - if (!toolNames.includes("search_memory")) { - console.error("❌ search_memory tool not found!"); - await client.close(); - process.exit(1); - } - - // Step 2: Test search_memory with various queries targeting existing graph data - const queries = [ - { label: "entity search: MyApp (should hit entity graph)", query: "MyApp" }, - { label: "entity search: PostgreSQL (should hit entity graph)", query: "PostgreSQL" }, - { label: "entity search: Redis (should hit entity graph)", query: "Redis" }, - { label: "entity search: AuthService (should hit entity graph)", query: "AuthService" }, - { label: "entity search: Kafka+EC2 (should hit entity graph)", query: "Kafka EC2 LogService" }, - { label: "semantic search: semantic memory about RAG gateway", query: "RAG gateway MCP server" }, - ]; - - for (const q of queries) { - console.log(`\n🔍 [${q.label}]`); - console.log(` Query: "${q.query}"`); - - const result = await client.callTool({ - name: "search_memory", - arguments: { - query: q.query, - limit: 5, - tenant_id: "default", - }, - }); - - const content = result.content as Array<{ type: string; text: string }>; - const text = content.map((c: { text: string }) => c.text).join("\n"); - console.log(` Result:\n${text}`); - - // Check if GraphRAG content is present - if (text.includes("Entity Graph")) { - console.log(" ✅ GraphRAG entity search RETURNED results!"); - } else { - console.log(" ⚠️ No GraphRAG results (entity graph may not contain this)"); - } - } - - await client.close(); - console.log("\n✅ Test complete."); -} - -main().catch((err) => { - console.error("❌ Test failed:", err); - process.exit(1); -}); diff --git a/rag-gateway/src/config.ts b/rag-gateway/src/config.ts deleted file mode 100644 index 4d52d4c..0000000 --- a/rag-gateway/src/config.ts +++ /dev/null @@ -1,134 +0,0 @@ -import "dotenv/config"; -import { readFileSync, existsSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, resolve } from "node:path"; - -export interface GatewayConfig { - port: number; - host: string; - databaseUrl?: string; - qdrantUrl?: string; - graphRagEnabled: boolean; - /** API key for embedding service (OpenAI-compatible) */ - embeddingApiKey: string; - /** Base URL for embedding service */ - embeddingBaseUrl: string; - /** Model name for embeddings (default: "bge-m3") */ - embeddingModel: string; - /** Vector dimension for Qdrant collection. Must match the embedding model used. - * bge-m3 = 1024, text-embedding-ada-002 = 1536, text-embedding-3-large = 3072 */ - vectorSize: number; - /** API key for entity extraction LLM (falls back to embeddingApiKey) */ - extractionApiKey: string; - /** Base URL for entity extraction LLM */ - extractionBaseUrl: string; - /** Model for entity extraction */ - extractionModel: string; -} - -function optionalEnv(key: string, fallback: string): string { - return process.env[key] ?? fallback; -} - -// ── env/platforms/rag-gateway.json loading ─────────────────────────────────── - -/** - * Apply parsed config env values to an env-like object. - * - * Keys in the JSON ARE the env var names directly (identity, no transform). - * Existing env values are never overwritten (dotenv takes precedence). - * Non-string values (null, numbers) are skipped. - * - * Pure function — no I/O, no side-effects on the outer process.env. - */ -export function applyGatewayConfigEnv( - values: Record, - env: Record, -): void { - for (const [key, value] of Object.entries(values)) { - if (typeof value === "string" && env[key] === undefined) { - env[key] = value; - } - } -} - -/** - * Read the gateway platform config from env/platforms/rag-gateway.json. - * The file structure is: - * { "env": { "EMBEDDING_API_KEY": "...", ... } } - */ -export function gatewayEnvFromConfig(): Record { - // Resolve env/platforms/rag-gateway.json relative to this source file. - // dist/config.js → ../../env/platforms/rag-gateway.json - const configPath = resolve( - dirname(fileURLToPath(import.meta.url)), - "../../env/platforms/rag-gateway.json", - ); - - if (!existsSync(configPath)) { - return {}; - } - - try { - const raw = readFileSync(configPath, "utf-8"); - const values: Record = JSON.parse(raw); - const env = values?.env; - return env !== null && typeof env === "object" && !Array.isArray(env) - ? (env as Record) - : {}; - } catch { - return {}; - } -} - -/** - * Load env/platforms/rag-gateway.json and apply env keys to process.env. - * .env values take precedence (already loaded by `import "dotenv/config"` above). - * Silently degrades to .env-only if the file is missing or malformed. - */ -function loadGatewayConfigJson(): void { - try { - const gatewayEnv = gatewayEnvFromConfig(); - applyGatewayConfigEnv(gatewayEnv, process.env); - const keyCount = Object.keys(gatewayEnv).length; - if (keyCount > 0) { - console.info(`[config] Loaded env/platforms/rag-gateway.json env (${keyCount} keys)`); - } - } catch (err) { - console.warn(`[config] Failed to load rag-gateway config: ${(err as Error).message}; using .env only`); - } -} - -export function loadConfig(): GatewayConfig { - // Phase 1: dotenv already ran at import-time (top of file). - // Phase 2: apply env/platforms/rag-gateway.json defaults for any keys still unset. - loadGatewayConfigJson(); - - const embeddingModel = optionalEnv("EMBEDDING_MODEL", "bge-m3"); - - // Infer vector dimension from model name when VECTOR_SIZE is not explicitly set - const rawVectorSize = process.env.VECTOR_SIZE ?? ""; - const DEFAULT_VECTOR_SIZES: Record = { - "bge-m3": 1024, - "text-embedding-ada-002": 1536, - "text-embedding-3-large": 3072, - }; - const vectorSize = rawVectorSize - ? parseInt(rawVectorSize, 10) - : (DEFAULT_VECTOR_SIZES[embeddingModel] ?? 1024); - - return { - port: parseInt(optionalEnv("GATEWAY_PORT", "3000"), 10), - host: optionalEnv("GATEWAY_HOST", "127.0.0.1"), - databaseUrl: optionalEnv("DATABASE_URL", ""), - qdrantUrl: optionalEnv("QDRANT_URL", ""), - graphRagEnabled: process.env.GRAPH_RAG_ENABLED === "true", - embeddingApiKey: optionalEnv("EMBEDDING_API_KEY", ""), - embeddingBaseUrl: optionalEnv("EMBEDDING_BASE_URL", "https://api.openai.com/v1"), - embeddingModel, - vectorSize, - extractionApiKey: optionalEnv("EXTRACTION_API_KEY", ""), - extractionBaseUrl: optionalEnv("EXTRACTION_BASE_URL", ""), - extractionModel: optionalEnv("EXTRACTION_MODEL", "gpt-4o-mini"), - }; -} diff --git a/rag-gateway/src/db/graph.ts b/rag-gateway/src/db/graph.ts deleted file mode 100644 index 3d69860..0000000 --- a/rag-gateway/src/db/graph.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * PostgreSQL CRUD for GraphRAG entity and entity_edge tables. - * - * Follows the same pattern as db/transcript.ts — one query module per domain, - * using getPool() for the connection pool. - */ -import crypto from "node:crypto"; -import { getPool } from "./index.js"; -import type { GraphEntity, GraphRelation } from "../types.js"; - -// ── Stop words filtered out during entity search tokenization ────── -const STOP_WORDS = new Set([ - "what", "which", "where", "when", "why", "how", "the", "this", "that", - "does", "is", "are", "was", "were", "will", "would", "could", "should", - "have", "has", "had", "do", "did", "can", "has", "for", "and", "not", - "with", "its", "use", "uses", "used", "using", "get", "got", "gets", -]); - -export interface StoreEntity { - id: string; - tenantId: string; - projectId?: string; - type: string; - name: string; - properties: Record; -} - -export interface StoreEdge { - id: string; - tenantId: string; - projectId?: string; - fromEntityId: string; - toEntityId: string; - relation: string; - properties: Record; -} - -/** - * Upsert a single entity by (name, tenant_id). - * If an entity with the same name + tenant exists, its type, project_id, and properties are updated. - * Returns the real persisted ID (the existing one when a conflict occurs). - */ -export async function upsertEntity(entity: StoreEntity): Promise { - const pool = getPool(); - const result = await pool.query( - `INSERT INTO entity (id, tenant_id, project_id, type, name, properties) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (name, tenant_id) DO UPDATE SET - type = COALESCE(NULLIF($4, ''), entity.type), - project_id = COALESCE(NULLIF($3, ''), entity.project_id), - properties = entity.properties || $6 - RETURNING id`, - [ - entity.id, - entity.tenantId, - entity.projectId ?? null, - entity.type, - entity.name, - JSON.stringify(entity.properties), - ], - ); - return result.rows[0].id as string; -} - -/** - * Batch upsert entities and return a map of name → persisted ID. - * - * Each entity that shares (name, tenant_id) with an existing row updates it - * and returns the real (existing) ID. New entities return their inserted ID. - * The returned map allows callers to build foreign-key references with the - * correct IDs rather than in-memory-generated IDs that ON CONFLICT discarded. - */ -export async function upsertEntities(entities: StoreEntity[]): Promise> { - if (entities.length === 0) return new Map(); - - const pool = getPool(); - const client = await pool.connect(); - const nameToId = new Map(); - try { - await client.query("BEGIN"); - for (const e of entities) { - const result = await client.query( - `INSERT INTO entity (id, tenant_id, project_id, type, name, properties) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (name, tenant_id) DO UPDATE SET - type = COALESCE(NULLIF($4, ''), entity.type), - project_id = COALESCE(NULLIF($3, ''), entity.project_id), - properties = entity.properties || $6 - RETURNING id, name`, - [ - e.id, - e.tenantId, - e.projectId ?? null, - e.type, - e.name, - JSON.stringify(e.properties), - ], - ); - const row = result.rows[0]; - nameToId.set(row.name as string, row.id as string); - } - await client.query("COMMIT"); - } catch (err) { - await client.query("ROLLBACK"); - throw err; - } finally { - client.release(); - } - return nameToId; -} - -/** - * Insert a single edge (idempotent — skips on conflict by id). - */ -export async function insertEdge(edge: StoreEdge): Promise { - const pool = getPool(); - await pool.query( - `INSERT INTO entity_edge (id, tenant_id, project_id, from_entity_id, to_entity_id, relation, properties) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO NOTHING`, - [ - edge.id, - edge.tenantId, - edge.projectId ?? null, - edge.fromEntityId, - edge.toEntityId, - edge.relation, - JSON.stringify(edge.properties), - ], - ); -} - -/** - * Batch insert edges. - */ -export async function insertEdges(edges: StoreEdge[]): Promise { - if (edges.length === 0) return; - - const pool = getPool(); - const client = await pool.connect(); - try { - await client.query("BEGIN"); - for (const e of edges) { - await client.query( - `INSERT INTO entity_edge (id, tenant_id, project_id, from_entity_id, to_entity_id, relation, properties) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO NOTHING`, - [ - e.id, - e.tenantId, - e.projectId ?? null, - e.fromEntityId, - e.toEntityId, - e.relation, - JSON.stringify(e.properties), - ], - ); - } - await client.query("COMMIT"); - } catch (err) { - await client.query("ROLLBACK"); - throw err; - } finally { - client.release(); - } -} - -/** - * Search entities by name or type (ILIKE matching). - * - * Tokenizes the query into individual words and searches each (OR'd). - * This allows queries like "what technologies does MyApp use?" to match - * entities named "MyApp" or "PostgreSQL". - */ -export async function searchEntities( - query: string, - tenantId: string, - options?: { limit?: number; projectId?: string }, -): Promise { - const pool = getPool(); - const limit = options?.limit ?? 5; - // Tokenize the query into individual words, filter short/common terms - const tokens = query - .toLowerCase() - .replace(/[^a-z0-9_\-\s]/g, " ") - .split(/\s+/) - .filter((t) => t.length >= 2) - .filter((t) => !STOP_WORDS.has(t)); - - if (tokens.length === 0) return []; - - // Build a combined ILIKE pattern for each token - const patterns = tokens.map((t) => `%${t}%`); - - let sql: string; - const params: unknown[] = [tenantId]; - - if (options?.projectId) { - params.push(options.projectId); - params.push(patterns); - params.push(limit); - sql = `SELECT id, tenant_id, project_id, type, name, properties, created_at - FROM entity - WHERE tenant_id = $1 AND project_id = $2 - AND (name ILIKE ANY($3::text[]) OR type ILIKE ANY($3::text[])) - ORDER BY name - LIMIT $4`; - } else { - params.push(patterns); - params.push(limit); - sql = `SELECT id, tenant_id, project_id, type, name, properties, created_at - FROM entity - WHERE tenant_id = $1 - AND (name ILIKE ANY($2::text[]) OR type ILIKE ANY($2::text[])) - ORDER BY name - LIMIT $3`; - } - - const result = await pool.query(sql, params); - - return result.rows.map(rowToEntity); -} - -/** - * Get entities by type. - */ -export async function getEntitiesByType( - type: string, - tenantId: string, - options?: { limit?: number }, -): Promise { - const pool = getPool(); - const limit = options?.limit ?? 20; - - const result = await pool.query( - `SELECT id, tenant_id, project_id, type, name, properties, created_at - FROM entity - WHERE tenant_id = $1 AND type = $2 - ORDER BY name - LIMIT $3`, - [tenantId, type, limit], - ); - - return result.rows.map(rowToEntity); -} - -/** - * Get the subgraph around a set of entity IDs (1-2 hop traversal). - * - * Strategy: - * 1. Fetch direct edges (1-hop) from/to the given entity IDs. - * 2. Collect all neighboring entity IDs from direct edges. - * 3. Fetch 2-hop edges from/to those neighbors (excluding already-seen edges). - * 4. Return merged array of all reachable entities and edges. - */ -export async function getSubgraph( - entityIds: string[], - tenantId: string, - options?: { maxHops?: number }, -): Promise<{ entities: GraphEntity[]; edges: GraphRelation[] }> { - if (entityIds.length === 0) return { entities: [], edges: [] }; - - const maxHops = options?.maxHops ?? 2; - const pool = getPool(); - - // Step 1: Get direct edges (1-hop) - const directEdges = await pool.query( - `SELECT id, tenant_id, project_id, from_entity_id, to_entity_id, relation, properties, created_at - FROM entity_edge - WHERE tenant_id = $1 - AND (from_entity_id = ANY($2::uuid[]) OR to_entity_id = ANY($2::uuid[]))`, - [tenantId, entityIds], - ); - - const allEdges = [...directEdges.rows.map(rowToRelation)]; - - if (maxHops >= 2) { - // Collect neighbor entity IDs - const neighborIds = new Set(); - for (const row of directEdges.rows) { - if (!entityIds.includes(row.from_entity_id)) neighborIds.add(row.from_entity_id); - if (!entityIds.includes(row.to_entity_id)) neighborIds.add(row.to_entity_id); - } - - if (neighborIds.size > 0) { - const neighborArray = Array.from(neighborIds); - - // Step 2: Get 2-hop edges involving neighbors (excluding direct edges) - const hop2Edges = await pool.query( - `SELECT id, tenant_id, project_id, from_entity_id, to_entity_id, relation, properties, created_at - FROM entity_edge - WHERE tenant_id = $1 - AND (from_entity_id = ANY($2::uuid[]) OR to_entity_id = ANY($2::uuid[])) - AND id <> ALL($3::uuid[])`, - [ - tenantId, - neighborArray, - directEdges.rows.length > 0 - ? directEdges.rows.map((r: { id: string }) => r.id) - : ["00000000-0000-0000-0000-000000000000"], - ], - ); - allEdges.push(...hop2Edges.rows.map(rowToRelation)); - } - } - - // Collect all reachable entity IDs - const reachableIds = new Set(entityIds); - for (const edge of allEdges) { - reachableIds.add(edge.fromEntityId); - reachableIds.add(edge.toEntityId); - } - - if (reachableIds.size === 0) return { entities: [], edges: allEdges }; - - // Fetch all reachable entities - const entitiesResult = await pool.query( - `SELECT id, tenant_id, project_id, type, name, properties, created_at - FROM entity - WHERE tenant_id = $1 AND id = ANY($2::uuid[])`, - [tenantId, Array.from(reachableIds)], - ); - - return { - entities: entitiesResult.rows.map(rowToEntity), - edges: allEdges, - }; -} - -/** - * Generate a UUID v4. - */ -export function generateId(): string { - return crypto.randomUUID(); -} - -// ── Row mappers ───────────────────────────────────────────────────── - -function rowToEntity(row: Record): GraphEntity { - return { - id: row.id as string, - tenantId: row.tenant_id as string, - projectId: (row.project_id as string) ?? undefined, - type: row.type as string, - name: row.name as string, - properties: typeof row.properties === "string" - ? JSON.parse(row.properties as string) - : (row.properties as Record), - createdAt: new Date(row.created_at as string), - }; -} - -function rowToRelation(row: Record): GraphRelation { - return { - id: row.id as string, - tenantId: row.tenant_id as string, - projectId: (row.project_id as string) ?? undefined, - fromEntityId: row.from_entity_id as string, - toEntityId: row.to_entity_id as string, - relation: row.relation as string, - properties: typeof row.properties === "string" - ? JSON.parse(row.properties as string) - : (row.properties as Record), - createdAt: new Date(row.created_at as string), - }; -} \ No newline at end of file diff --git a/rag-gateway/src/db/index.ts b/rag-gateway/src/db/index.ts deleted file mode 100644 index c4786d8..0000000 --- a/rag-gateway/src/db/index.ts +++ /dev/null @@ -1,139 +0,0 @@ -import pg from "pg"; - -const { Pool } = pg; - -let pool: pg.Pool | null = null; - -export function getPool(): pg.Pool { - if (!pool) { - throw new Error("Database not initialized. Call initDb() first."); - } - return pool; -} - -export function initDb(databaseUrl: string): pg.Pool { - pool = new Pool({ connectionString: databaseUrl }); - return pool; -} - -export async function closeDb(): Promise { - if (pool) { - await pool.end(); - pool = null; - } -} - -/** - * Run schema migrations on startup (idempotent). - * - * We create the tables if they don't exist, and add any missing columns - * via DO $$ blocks so this is safe to run on every boot during development. - */ -export async function migrateSchema(): Promise { - const client = await getPool().connect(); - try { - await client.query("BEGIN"); - - // ── conversations ───────────────────────────────────────────── - await client.query( - `CREATE TABLE IF NOT EXISTS conversation ( - id UUID PRIMARY KEY, - tenant_id TEXT NOT NULL DEFAULT 'default', - project_id TEXT, - client TEXT NOT NULL DEFAULT 'unknown', - model TEXT NOT NULL, - started_at TIMESTAMPTZ NOT NULL DEFAULT now(), - ended_at TIMESTAMPTZ, - token_count INTEGER DEFAULT 0 - );` - ); - - // ── messages ────────────────────────────────────────────────── - await client.query( - `CREATE TABLE IF NOT EXISTS message ( - id UUID PRIMARY KEY, - conversation_id UUID NOT NULL REFERENCES conversation(id) ON DELETE CASCADE, - turn_index INTEGER NOT NULL DEFAULT 0, - role TEXT NOT NULL, - content TEXT, - tool_calls_json JSONB, - tool_call_id TEXT, - name TEXT, - token_count INTEGER DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL DEFAULT now() - );` - ); - - // ── indexes (idempotent via IF NOT EXISTS in PG 9.5+) ───────── - await client.query( - `CREATE INDEX IF NOT EXISTS idx_message_conversation ON message(conversation_id, turn_index);` - ); - - await client.query( - `CREATE INDEX IF NOT EXISTS idx_conversation_tenant_started ON conversation(tenant_id, started_at DESC);` - ); - - // ── entity (GraphRAG) ─────────────────────────────────────────── - await client.query( - `CREATE TABLE IF NOT EXISTS entity ( - id UUID PRIMARY KEY, - tenant_id TEXT NOT NULL DEFAULT 'default', - project_id TEXT, - type TEXT NOT NULL, - name TEXT NOT NULL, - properties JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT now() - );` - ); - - // ── entity_edge (GraphRAG) ────────────────────────────────────── - await client.query( - `CREATE TABLE IF NOT EXISTS entity_edge ( - id UUID PRIMARY KEY, - tenant_id TEXT NOT NULL DEFAULT 'default', - project_id TEXT, - from_entity_id UUID NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - to_entity_id UUID NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - relation TEXT NOT NULL, - properties JSONB NOT NULL DEFAULT '{}', - created_at TIMESTAMPTZ NOT NULL DEFAULT now() - );` - ); - - // ── GraphRAG indexes ──────────────────────────────────────────── - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_name ON entity(name);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_type ON entity(type);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_tenant ON entity(tenant_id);` - ); - await client.query( - `CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_name_tenant ON entity(name, tenant_id);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_tenant_project ON entity(tenant_id, project_id);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_edge_from ON entity_edge(from_entity_id);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_edge_to ON entity_edge(to_entity_id);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_entity_edge_relation ON entity_edge(relation);` - ); - await client.query( - `CREATE INDEX IF NOT EXISTS idx_edge_tenant_project ON entity_edge(tenant_id, project_id);` - ); - - await client.query("COMMIT"); - } catch (err) { - await client.query("ROLLBACK"); - throw err; - } finally { - client.release(); - } -} \ No newline at end of file diff --git a/rag-gateway/src/db/transcript.ts b/rag-gateway/src/db/transcript.ts deleted file mode 100644 index bb7b245..0000000 --- a/rag-gateway/src/db/transcript.ts +++ /dev/null @@ -1,88 +0,0 @@ -import crypto from "node:crypto"; -import { getPool } from "./index.js"; - -export interface StoredConversation { - id: string; - tenantId: string; - projectId?: string; - client: string; - model: string; - startedAt: Date; -} - -export interface StoredMessage { - id: string; - conversationId: string; - turnIndex: number; - role: string; - content: string | null; - toolCallsJson?: unknown; - toolCallId?: string; - name?: string; - tokenCount?: number; -} - -/** - * Insert a new conversation record. - */ -export async function insertConversation( - conv: StoredConversation, -): Promise { - const pool = getPool(); - await pool.query( - `INSERT INTO conversation (id, tenant_id, project_id, client, model, started_at) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (id) DO NOTHING`, - [ - conv.id, - conv.tenantId, - conv.projectId ?? null, - conv.client, - conv.model, - conv.startedAt, - ], - ); -} - -/** - * Append a message to a conversation. - */ -export async function insertMessage(msg: StoredMessage): Promise { - const pool = getPool(); - await pool.query( - `INSERT INTO message (id, conversation_id, turn_index, role, content, tool_calls_json, tool_call_id, name, token_count) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [ - msg.id, - msg.conversationId, - msg.turnIndex, - msg.role, - msg.content, - msg.toolCallsJson ? JSON.stringify(msg.toolCallsJson) : null, - msg.toolCallId ?? null, - msg.name ?? null, - msg.tokenCount ?? null, - ], - ); -} - -/** - * Update the end timestamp and total token count on a conversation. - */ -export async function finalizeConversation( - conversationId: string, - totalTokens: number, -): Promise { - const pool = getPool(); - await pool.query( - `UPDATE conversation SET ended_at = now(), token_count = $2 WHERE id = $1`, - [conversationId, totalTokens], - ); -} - -/** - * Generate a UUID v4. - */ -export function generateId(): string { - return crypto.randomUUID(); -} \ No newline at end of file diff --git a/rag-gateway/src/entity/extractor.ts b/rag-gateway/src/entity/extractor.ts deleted file mode 100644 index f462e95..0000000 --- a/rag-gateway/src/entity/extractor.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Entity Extractor — uses the LLM to extract structured entities and - * relationships from conversation text. - * - * Runs as a fire-and-forget background step after each MCP tool call. - * Gracefully degrades: on failure, returns null and the caller logs - * the degradation but does not block the response path. - * - * Configured via EXTRACTION_API_KEY / EXTRACTION_BASE_URL / EXTRACTION_MODEL. - * Falls back to EMBEDDING_API_KEY / EMBEDDING_BASE_URL if extraction-specific - * env vars are unset. - */ -import OpenAI from "openai"; -import type { GatewayConfig } from "../config.js"; -import type { ExtractionResult } from "../types.js"; - -const EXTRACTION_PROMPT = `You are an entity and relationship extractor for a software architecture knowledge graph. - -Analyze the conversation text below and extract: - -1. ENTITIES: Named things that represent a distinct concept in the conversation. - Entity types: "project", "api", "service", "database", "technology", "decision", "file", "module", "tool", "person", "pattern", "config" - - Each entity must have: - - type: one of the listed types - - name: the canonical name or identifier - - properties: key-value pairs of relevant attributes (e.g. {"language":"TypeScript", "version":"2.0"}) - -2. RELATIONSHIPS: Directed connections between entities. - Relationship types: "uses", "depends_on", "implements", "extends", "references", "applies_to", "mentions", "installed", "configured", "deployed_on" - - Each relationship must have: - - from: the name of the source entity - - to: the name of the target entity - - relation: one of the listed relationship types - - properties: key-value pairs (e.g. {"context": "production deployment"}) - -Rules: -- Only extract entities that are explicitly mentioned or clearly implied by the text. -- Deduplicate by name: if the same entity name appears multiple times, include it once. -- Skip generic terms like "the app", "the database", "the project" unless given a specific name. -- Properties should only contain factual attributes stated in the text. -- If no entities are found, return {"entities": [], "relationships": []}. - -Output strictly as JSON with no other text: -{ - "entities": [ - {"type": "...", "name": "...", "properties": {}} - ], - "relationships": [ - {"from": "...", "to": "...", "relation": "...", "properties": {}} - ] -}`; - -/** - * Extracts entities and relationships from conversation text using the LLM. - */ -export class EntityExtractor { - private client: OpenAI; - private model: string; - - constructor(config: GatewayConfig) { - this.client = new OpenAI({ - apiKey: config.extractionApiKey || config.embeddingApiKey, - baseURL: config.extractionBaseUrl || config.embeddingBaseUrl, - }); - this.model = config.extractionModel; - } - - /** - * Extract structured entities and relationships from text. - * Returns null on failure (parse error, API error, empty response). - */ - async extract(text: string): Promise { - // Skip very short text — unlikely to contain meaningful entities - if (!text || text.length < 20) return null; - - try { - const response = await this.client.chat.completions.create({ - model: this.model, - messages: [ - { role: "system", content: EXTRACTION_PROMPT }, - { role: "user", content: text }, - ], - response_format: { type: "json_object" }, - max_tokens: 1000, - temperature: 0.1, - }); - - const content = response.choices[0]?.message?.content; - if (!content) return null; - - const parsed = JSON.parse(content) as ExtractionResult; - - // Validate shape - if (!Array.isArray(parsed.entities) || !Array.isArray(parsed.relationships)) { - return null; - } - - return parsed; - } catch (err) { - console.error("Entity extraction error:", (err as Error).message); - return null; - } - } -} \ No newline at end of file diff --git a/rag-gateway/src/entity/store.ts b/rag-gateway/src/entity/store.ts deleted file mode 100644 index e4c4581..0000000 --- a/rag-gateway/src/entity/store.ts +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Entity Store — high-level orchestrator for GraphRAG entity extraction, - * graph search, and context formatting. - * - * Connects the EntityExtractor (LLM), db/graph.ts (PostgreSQL), and - * metrics pipeline into a single interface used by the chat route. - */ -import type { GatewayConfig } from "../config.js"; -import { EntityExtractor } from "./extractor.js"; -import * as graphDb from "../db/graph.js"; -import { metricsCollector } from "../metrics.js"; -import type { GraphSearchResult, GraphEntity, GraphRelation } from "../types.js"; -import crypto from "node:crypto"; - -export class EntityStore { - private extractor: EntityExtractor; - private enabled: boolean; - - constructor(config: GatewayConfig) { - this.extractor = new EntityExtractor(config); - this.enabled = config.graphRagEnabled; - } - - get isEnabled(): boolean { - return this.enabled; - } - - /** - * Extract entities and relationships from conversation text and store - * them in the PostgreSQL graph. - * - * This is intended to be called fire-and-forget — errors are logged - * but never propagated to the caller. - * - * Steps: - * 1. Call the LLM extractor to get structured entities + relationships. - * 2. Deduplicate entities by name within this batch. - * 3. Upsert entities (ON CONFLICT handles cross-batch dedup by name+tenant). - * 4. Insert edges between resolved entities. - */ - async extractAndStore( - text: string, - tenantId: string, - projectId?: string, - ): Promise { - if (!this.enabled) return; - - try { - const result = await this.extractor.extract(text); - if (!result || result.entities.length === 0) return; - - // Deduplicate entities by name within this batch - const nameToId = new Map(); - - const entities = result.entities - .filter((e) => e.name && e.name.length > 0) - .map((e) => { - const normalizedName = e.name.trim(); - const existingId = nameToId.get(normalizedName); - if (existingId) { - // Duplicate within batch — skip - return null; - } - const id = crypto.randomUUID(); - nameToId.set(normalizedName, id); - return { - id, - tenantId, - projectId, - type: e.type, - name: normalizedName, - properties: e.properties, - }; - }) - .filter((e): e is NonNullable => e !== null); - - let persistedIds: Map | undefined; - if (entities.length > 0) { - persistedIds = await graphDb.upsertEntities(entities); - } - - // Merge persisted IDs so ON CONFLICT-updated entities use their real DB id, - // preventing FK violations when the in-memory id was discarded by ON CONFLICT. - const resolvedNameToId = new Map(nameToId); - if (persistedIds) { - for (const [name, id] of persistedIds) { - resolvedNameToId.set(name, id); - } - } - - // Insert edges only where both endpoints are resolved in this batch - const edges = result.relationships - .filter((r) => { - const fromId = resolvedNameToId.get(r.from.trim()); - const toId = resolvedNameToId.get(r.to.trim()); - return fromId && toId && r.relation && r.relation.length > 0; - }) - .map((r) => ({ - id: crypto.randomUUID(), - tenantId, - projectId, - fromEntityId: resolvedNameToId.get(r.from.trim())!, - toEntityId: resolvedNameToId.get(r.to.trim())!, - relation: r.relation, - properties: r.properties, - })); - - if (edges.length > 0) { - await graphDb.insertEdges(edges); - } - - metricsCollector.recordEntityExtraction(entities.length); - } catch (err) { - console.error("Entity extraction/storage error:", (err as Error).message); - } - } - - /** - * Search the entity graph: find entities matching the query text, - * then retrieve their 1-2 hop neighborhood. - * - * Returns GraphSearchResult objects, each containing the matched entity - * and its connected relations (with both endpoint entities resolved). - */ - async searchGraph( - query: string, - tenantId: string, - options?: { limit?: number; projectId?: string }, - ): Promise { - if (!this.enabled) return []; - if (!query || query.length === 0) return []; - - try { - // Step 1: Find matching entities by name/type ILIKE - const entities = await graphDb.searchEntities(query, tenantId, { - limit: options?.limit ?? 5, - projectId: options?.projectId, - }); - - if (entities.length === 0) return []; - - // Step 2: Get the 1-2 hop subgraph around these entities - const subgraph = await graphDb.getSubgraph( - entities.map((e) => e.id), - tenantId, - ); - - // Step 3: Build a lookup map for all entities in the subgraph - const entityMap = new Map(); - for (const e of subgraph.entities) { - entityMap.set(e.id, e); - } - // Ensure the matched entities are in the map (they should be) - for (const e of entities) { - if (!entityMap.has(e.id)) { - entityMap.set(e.id, e); - } - } - - // Step 4: Build GraphSearchResult for each matched entity - return entities.map((entity) => ({ - entity, - relations: subgraph.edges - .filter( - (e) => e.fromEntityId === entity.id || e.toEntityId === entity.id, - ) - .map((edge) => ({ - relation: edge, - fromEntity: - entityMap.get(edge.fromEntityId) ?? createPlaceholderEntity(edge.fromEntityId), - toEntity: - entityMap.get(edge.toEntityId) ?? createPlaceholderEntity(edge.toEntityId), - })), - })); - } catch (err) { - console.error("Graph search error:", (err as Error).message); - return []; - } - } - - /** - * Format graph search results as structured context strings - * for injection into the system prompt. - * - * Output format: - * [entity] "EntityName" (type) - * [entity-relationship] "FromEntity" —[relation]→ "ToEntity" - */ - formatContext(results: GraphSearchResult[]): string[] { - const lines: string[] = []; - const seenRelations = new Set(); - - for (const r of results) { - lines.push(`[entity] "${r.entity.name}" (${r.entity.type})`); - - for (const rel of r.relations) { - const key = `${rel.relation.id}:${rel.fromEntity.name}:${rel.toEntity.name}`; - if (seenRelations.has(key)) continue; - seenRelations.add(key); - - lines.push( - `[entity-relationship] "${rel.fromEntity.name}" —[${rel.relation.relation}]→ "${rel.toEntity.name}"`, - ); - } - } - - return lines; - } -} - -/** - * Create a minimal placeholder entity for an ID that exists in an edge - * but wasn't returned in the subgraph query. This should not normally occur - * given FK constraints, but guards against edge cases during data migration. - */ -function createPlaceholderEntity(id: string): GraphEntity { - return { - id, - tenantId: "unknown", - type: "unknown", - name: id, - properties: {}, - createdAt: new Date(), - }; -} \ No newline at end of file diff --git a/rag-gateway/src/index.ts b/rag-gateway/src/index.ts deleted file mode 100644 index f98b849..0000000 --- a/rag-gateway/src/index.ts +++ /dev/null @@ -1,124 +0,0 @@ -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import { loadConfig } from "./config.js"; -import { initDb, migrateSchema } from "./db/index.js"; -import { EmbeddingService } from "./vector/embedding.js"; -import { QdrantStore } from "./vector/qdrant.js"; -import { VectorStore } from "./vector/store.js"; -import { ToolRegistry } from "./tool/registry.js"; -import { ToolExecutorEngine } from "./tool/executor.js"; -import { EntityStore } from "./entity/store.js"; -import { metricsCollector } from "./metrics.js"; -import { registerMcpServer } from "./mcp/server.js"; - -async function main() { - const config = loadConfig(); - - const app = Fastify({ - logger: { - level: "info", - }, - }); - - // -- CORS (permissive for MVP; tighten in production) -------------------- - await app.register(cors, { - origin: true, - }); - - // Track startup degradations - const startupDegradations: string[] = []; - - // -- Database (optional - degrade gracefully) --------------------------- - if (config.databaseUrl) { - try { - initDb(config.databaseUrl); - await migrateSchema(); - app.log.info("PostgreSQL connected and schema migrated"); - } catch (err) { - const msg = (err as Error).message; - app.log.warn({ err }, "PostgreSQL unavailable"); - startupDegradations.push("postgres"); - metricsCollector.recordDegradation("postgres", msg); - } - } else { - app.log.info("DATABASE_URL not set — entity store disabled"); - } - - // -- Qdrant semantic memory (optional - degrade gracefully) ------------- - let vectorStore: VectorStore | undefined; - if (config.qdrantUrl) { - try { - const embedding = new EmbeddingService(config); - const qdrant = new QdrantStore(config.qdrantUrl, { vectorSize: config.vectorSize }); - await qdrant.ensureCollection(); - await qdrant.ensurePayloadIndexes(); - vectorStore = new VectorStore(embedding, qdrant); - app.log.info("Qdrant semantic memory ready"); - } catch (err) { - const msg = (err as Error).message; - app.log.warn({ err }, "Qdrant unavailable - semantic memory disabled"); - startupDegradations.push("qdrant"); - metricsCollector.recordDegradation("qdrant", msg); - } - } else { - app.log.info("QDRANT_URL not set - semantic memory disabled"); - } - - // -- Entity store / GraphRAG (optional - requires DB + config flag) ----- - let entityStore: EntityStore | undefined; - if (config.databaseUrl && config.graphRagEnabled) { - try { - entityStore = new EntityStore(config); - app.log.info("GraphRAG entity store ready"); - } catch (err) { - const msg = (err as Error).message; - app.log.warn({ err }, "GraphRAG unavailable - entity extraction disabled"); - startupDegradations.push("entity-extraction"); - metricsCollector.recordDegradation("entity-extraction", msg); - } - } else { - app.log.info( - config.graphRagEnabled - ? "DATABASE_URL not set - GraphRAG disabled" - : "GRAPH_RAG_ENABLED not set - GraphRAG disabled", - ); - } - - // -- Declarative tool registry ----------------------------------------- - const toolRegistry = new ToolRegistry(); - toolRegistry.loadFromFile(); - app.log.info(`Tool registry: ${toolRegistry.count} tools loaded`); - - // -- Tool executor (injected with stores for memory_search executor) ---- - const toolExecutor = new ToolExecutorEngine(undefined, { vectorStore, entityStore }); - - // -- Health check ------------------------------------------------------- - app.get("/health", async () => { - return { status: "ok", timestamp: new Date().toISOString() }; - }); - - // -- Metrics endpoint --------------------------------------------------- - app.get("/metrics", async () => { - return metricsCollector.snapshot(); - }); - - // -- MCP server (tools/list + tools/call via SSE) ---------------------- - registerMcpServer(app, toolRegistry, toolExecutor); - - // -- Start -------------------------------------------------------------- - const address = await app.listen({ port: config.port, host: config.host }); - app.log.info({ address, degradations: startupDegradations }, "Gateway started"); - - // -- Graceful shutdown -------------------------------------------------- - const shutdown = async () => { - app.log.info("Shutting down..."); - await app.close(); - }; - process.on("SIGTERM", shutdown); - process.on("SIGINT", shutdown); -} - -main().catch((err) => { - console.error("Failed to start gateway:", err); - process.exit(1); -}); \ No newline at end of file diff --git a/rag-gateway/src/mcp/client.ts b/rag-gateway/src/mcp/client.ts deleted file mode 100644 index 3cfec14..0000000 --- a/rag-gateway/src/mcp/client.ts +++ /dev/null @@ -1,208 +0,0 @@ -/** - * McpClientManager — manages outbound MCP client connections. - * - * Connects to external MCP servers (stdio subprocesses or SSE URL endpoints) - * and exposes their tool lists for integration with the gateway's ToolRegistry. - */ - -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -import { metricsCollector } from "../metrics.js"; -import type { McpServerConfig } from "./config.js"; - -export type McpConnectionState = "connecting" | "connected" | "disconnected" | "failed"; - -interface McpServerEntry { - config: McpServerConfig; - client: Client; - transport: StdioClientTransport | SSEClientTransport; - state: McpConnectionState; - tools: Tool[]; - error?: string; -} - -/** - * Managed outbound MCP client that handles lazy connection and lifecycle. - */ -export class McpClientManager { - private servers: Map = new Map(); - private started = false; - - constructor() { } - - /** - * Register all servers from the config map. Call `startAll()` to connect. - */ - registerServers(configs: Map): void { - for (const [name, config] of configs) { - if (this.servers.has(name)) continue; - - const client = new Client( - { name: "universal-rag-gateway", version: "0.1.0" }, - { capabilities: {} }, - ); - - const entry: McpServerEntry = { - config, - client, - transport: null as unknown as StdioClientTransport | SSEClientTransport, - state: "disconnected", - tools: [], - }; - this.servers.set(name, entry); - } - } - - /** - * Connect to all registered servers eagerly on startup. - */ - async startAll(): Promise { - if (this.started) return; - this.started = true; - - const results = await Promise.allSettled( - Array.from(this.servers.entries()).map(async ([name]) => { - await this.connect(name); - }), - ); - - const connected = results.filter((r) => r.status === "fulfilled").length; - const failed = results.filter((r) => r.status === "rejected").length; - console.log(`🔌 MCP outbound: ${connected} connected, ${failed} failed`); - - // Record degradations for failed servers - for (let i = 0; i < results.length; i++) { - const entry = Array.from(this.servers.values())[i]; - if (results[i].status === "rejected" && entry) { - const reason = (results[i] as PromiseRejectedResult).reason; - entry.state = "failed"; - entry.error = String(reason); - metricsCollector.recordDegradation("mcp-" + (entry.config.command || "url"), String(reason)); - } - } - } - - /** - * Connect a single server by name. - */ - async connect(name: string): Promise { - const entry = this.servers.get(name); - if (!entry) throw new Error(`Unknown MCP server: ${name}`); - if (entry.state === "connected") return; - - entry.state = "connecting"; - - try { - let transport: StdioClientTransport | SSEClientTransport; - - if (entry.config.command) { - transport = new StdioClientTransport({ - command: entry.config.command, - args: entry.config.args, - env: entry.config.env, - }); - } else if (entry.config.url) { - const url = new URL(entry.config.url); - transport = new SSEClientTransport(url, { - requestInit: entry.config.headers - ? { headers: entry.config.headers as Record } - : undefined, - }); - } else { - throw new Error(`MCP server "${name}" has neither command nor url`); - } - - entry.transport = transport; - await entry.client.connect(transport); - - // Cache tool list - try { - const result = await entry.client.listTools(); - entry.tools = result.tools ?? []; - console.log(` 📋 MCP "${name}": ${entry.tools.length} tools available`); - } catch { - console.warn(` ⚠️ MCP "${name}": could not list tools`); - entry.tools = []; - } - - entry.state = "connected"; - entry.error = undefined; - } catch (err) { - entry.state = "failed"; - entry.error = (err as Error).message; - throw err; - } - } - - /** - * Get a connected Client by server name. - * Returns undefined if the server is not connected or doesn't exist. - */ - getClient(name: string): Client | undefined { - const entry = this.servers.get(name); - if (!entry || entry.state !== "connected") return undefined; - return entry.client; - } - - /** - * Get the connection state for a server. - */ - getState(name: string): McpConnectionState { - return this.servers.get(name)?.state ?? "disconnected"; - } - - /** - * Get the cached tool list for a server. - */ - getServerTools(name: string): Tool[] { - return this.servers.get(name)?.tools ?? []; - } - - /** - * Get all tools from all connected servers, keyed by server name. - * Returns { serverName: tool[] }. - */ - getAllTools(): Map { - const result = new Map(); - for (const [name, entry] of this.servers) { - if (entry.state === "connected") { - result.set(name, entry.tools); - } - } - return result; - } - - /** - * Close all client connections. - */ - async closeAll(): Promise { - for (const [name, entry] of this.servers) { - try { - await entry.client.close(); - entry.state = "disconnected"; - } catch (err) { - console.error(`Error closing MCP client "${name}":`, (err as Error).message); - } - } - } - - /** - * Number of registered servers. - */ - get count(): number { - return this.servers.size; - } - - /** - * Number of connected servers. - */ - get connectedCount(): number { - let count = 0; - for (const entry of this.servers.values()) { - if (entry.state === "connected") count++; - } - return count; - } -} \ No newline at end of file diff --git a/rag-gateway/src/mcp/config.ts b/rag-gateway/src/mcp/config.ts deleted file mode 100644 index 73e3f7d..0000000 --- a/rag-gateway/src/mcp/config.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * MCP server configuration types and loader. - * - * Reads any JSON file with an `mcpServers` object. - * The canonical local source is `env/secrets.json`: - * - * { - * "mcpServers": { - * "server-name": { - * "command": "npx", - * "args": ["-y", "some-server"], - * "env": { "KEY": "value" } - * } - * } - * } - * - * Or: - * - * { - * "mcpServers": { - * "server-name": { - * "url": "https://example.com/mcp", - * "headers": { "Authorization": "Bearer ..." } - * } - * } - * } - */ - -import fs from "node:fs"; - -export interface McpServerConfig { - /** For stdio-based servers: executable to spawn */ - command?: string; - /** Arguments passed to the command */ - args?: string[]; - /** Environment variables for the spawned process */ - env?: Record; - /** For URL-based servers: SSE endpoint */ - url?: string; - /** HTTP headers for URL-based connections */ - headers?: Record; -} - -export interface McpServersFile { - mcpServers: Record; -} - -/** - * Load MCP server configurations from a JSON file. - * Returns an empty map if the file doesn't exist or can't be parsed. - */ -export function loadMcpServerConfigs(filePath: string): Map { - const configs = new Map(); - - if (!fs.existsSync(filePath)) { - console.warn(`⚠️ MCP servers file not found: ${filePath}`); - return configs; - } - - try { - const raw = JSON.parse(fs.readFileSync(filePath, "utf-8")) as McpServersFile; - - if (!raw.mcpServers || typeof raw.mcpServers !== "object") { - console.warn("⚠️ MCP servers file has unexpected format — expected { mcpServers: { ... } }"); - return configs; - } - - for (const [name, config] of Object.entries(raw.mcpServers)) { - if (!config.command && !config.url) { - console.warn(`⚠️ MCP server "${name}" has no command or url — skipping`); - continue; - } - configs.set(name, config); - } - - console.log(`🔌 MCP server configs loaded: ${configs.size} servers`); - } catch (err) { - console.error("Failed to load MCP servers file:", (err as Error).message); - } - - return configs; -} diff --git a/rag-gateway/src/mcp/server.ts b/rag-gateway/src/mcp/server.ts deleted file mode 100644 index 519e2a9..0000000 --- a/rag-gateway/src/mcp/server.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Inbound MCP server adapter — exposes the gateway as an MCP server. - * - * Uses SSE transport for broad client compatibility (Cursor, VS Code MCP support). - * - * Routes: - * GET /mcp/sse → establishes SSE stream - * POST /mcp/message → receives JSON-RPC messages - * - * Handlers: - * tools/list → returns all active tools from ToolRegistry - * tools/call → delegates to ToolExecutorEngine - */ - -import type { FastifyInstance } from "fastify"; -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; -import { - ListToolsRequestSchema, - CallToolRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import type { ToolRegistry } from "../tool/registry.js"; -import type { ToolExecutorEngine } from "../tool/executor.js"; - -/** - * Register MCP server routes on a Fastify instance using SSE transport. - * - * Each SSE connection creates its own Server + transport pair so that - * concurrent clients do not collide on the SDK's single-transport limit. - */ -export function registerMcpServer( - app: FastifyInstance, - toolRegistry: ToolRegistry, - toolExecutor: ToolExecutorEngine, -): void { - const sessions = new Map(); - - // ── Fastify route: GET /mcp/sse (establish SSE stream) ────────── - app.get("/mcp/sse", async (_request, reply) => { - const transport = new SSEServerTransport("/mcp/message", reply.raw); - sessions.set(transport.sessionId, transport); - - // Create a fresh Server instance per connection - const mcpServer = new Server( - { name: "universal-rag-gateway", version: "0.1.0" }, - { capabilities: { tools: {} } }, - ); - - // ── tools/list handler ───────────────────────────────────────────── - mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { - const specs = toolRegistry.getActiveTools(); - const tools = specs.map((s) => ({ - name: s.name, - description: s.description, - inputSchema: s.input_schema as { - type: "object"; - properties: Record; - required?: string[]; - }, - })); - return { tools }; - }); - - // ── tools/call handler ───────────────────────────────────────────── - mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - const spec = toolRegistry.get(name); - if (!spec) { - return { - content: [{ type: "text" as const, text: `Unknown tool: ${name}` }], - isError: true, - }; - } - - const result = await toolExecutor.execute( - spec, - "mcp-inbound", - (args ?? {}) as Record, - ); - - return { - content: [ - { - type: "text" as const, - text: result.success - ? result.content - : `Error: ${result.error ?? result.content}`, - }, - ], - isError: !result.success, - }; - }); - - reply.raw.on("close", () => { - sessions.delete(transport.sessionId); - }); - - await mcpServer.connect(transport); - }); - - // ── Fastify route: POST /mcp/message (receive JSON-RPC messages) ─ - app.post("/mcp/message", async (request, reply) => { - const query = request.query as Record; - const sessionId = query.sessionId; - - const transport = sessionId ? sessions.get(sessionId) : undefined; - if (!transport) { - return reply.status(404).send({ - error: `No MCP session found${sessionId ? `: ${sessionId}` : ""}`, - }); - } - - await transport.handlePostMessage( - request.raw, - reply.raw, - request.body as Record, - ); - }); - - app.log.info("MCP inbound server registered at GET /mcp/sse and POST /mcp/message"); -} \ No newline at end of file diff --git a/rag-gateway/src/metrics.ts b/rag-gateway/src/metrics.ts deleted file mode 100644 index 192607d..0000000 --- a/rag-gateway/src/metrics.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * In-memory metrics collector for the gateway. - * - * Exposes a simple accumulator that track counts, histograms, and - * recent error states. Designed to be: - * - Zero-dependency (no Prometheus client library needed) - * - Lossy under high load (ring buffer for latencies) - * - Queryable via GET /metrics as JSON - * - * Enable with `GATEWAY_METRICS_ENABLED=true` in the environment. - */ - -const MAX_LATENCY_SAMPLES = 1000; -const MAX_TELEMETRY_RECORDS = 100; - -export interface MetricsSnapshot { - uptimeMs: number; - requestsTotal: number; - requestsByModel: Record; - requestsByStatus: Record; - latencyHistogramMs: HistogramBuckets; - toolCallsTotal: number; - retrievalHitsTotal: number; - entitiesExtractedTotal: number; - degradationCount: number; - recentDegradations: Array<{ component: string; reason: string; time: string }>; -} - -export interface HistogramBuckets { - "0-100": number; - "100-500": number; - "500-2000": number; - "2000-5000": number; - "5000+": number; -} - -class MetricsCollector { - private startTime = Date.now(); - private requestsTotal = 0; - private requestsByModel: Record = {}; - private requestsByStatus: Record = {}; - private latencySamples: number[] = []; - private toolCallsTotal = 0; - private retrievalHitsTotal = 0; - private entitiesExtractedTotal = 0; - private recentDegradations: Array<{ component: string; reason: string; time: string }> = []; - - recordRequest(model: string, latencyMs: number, status: string): void { - this.requestsTotal++; - this.requestsByModel[model] = (this.requestsByModel[model] ?? 0) + 1; - this.requestsByStatus[status] = (this.requestsByStatus[status] ?? 0) + 1; - - if (this.latencySamples.length < MAX_LATENCY_SAMPLES) { - this.latencySamples.push(latencyMs); - } - // When full, drop — we don't need perfect accuracy for MVP - } - - recordToolCall(count: number): void { - this.toolCallsTotal += count; - } - - recordRetrievalHits(count: number): void { - this.retrievalHitsTotal += count; - } - - recordEntityExtraction(count: number): void { - this.entitiesExtractedTotal += count; - } - - recordDegradation(component: string, reason: string): void { - if (this.recentDegradations.length >= MAX_TELEMETRY_RECORDS) { - this.recentDegradations.shift(); - } - this.recentDegradations.push({ - component, - reason, - time: new Date().toISOString(), - }); - } - - snapshot(): MetricsSnapshot { - return { - uptimeMs: Date.now() - this.startTime, - requestsTotal: this.requestsTotal, - requestsByModel: { ...this.requestsByModel }, - requestsByStatus: { ...this.requestsByStatus }, - latencyHistogramMs: this.computeHistogram(this.latencySamples), - toolCallsTotal: this.toolCallsTotal, - retrievalHitsTotal: this.retrievalHitsTotal, - entitiesExtractedTotal: this.entitiesExtractedTotal, - degradationCount: this.recentDegradations.length, - recentDegradations: [...this.recentDegradations], - }; - } - - private computeHistogram(samples: number[]): HistogramBuckets { - const buckets: HistogramBuckets = { - "0-100": 0, - "100-500": 0, - "500-2000": 0, - "2000-5000": 0, - "5000+": 0, - }; - - for (const ms of samples) { - if (ms <= 100) buckets["0-100"]++; - else if (ms <= 500) buckets["100-500"]++; - else if (ms <= 2000) buckets["500-2000"]++; - else if (ms <= 5000) buckets["2000-5000"]++; - else buckets["5000+"]++; - } - - return buckets; - } -} - -/** Singleton collector instance */ -export const metricsCollector = new MetricsCollector(); \ No newline at end of file diff --git a/rag-gateway/src/tool/executor.ts b/rag-gateway/src/tool/executor.ts deleted file mode 100644 index db0c625..0000000 --- a/rag-gateway/src/tool/executor.ts +++ /dev/null @@ -1,312 +0,0 @@ -import type { ToolSpec, ToolExecutor } from "./types.js"; -import type { VectorStore } from "../vector/store.js"; -import type { EntityStore } from "../entity/store.js"; -import type { QdrantSearchResult } from "../vector/qdrant.js"; -import type { GraphSearchResult } from "../types.js"; - -export interface ToolExecutionResult { - toolCallId: string; - name: string; - content: string; - success: boolean; - error?: string; -} - -/** - * ToolExecutorEngine runs declarative tool specs. - * - * For MVP, supports: - * - http_request (allowlisted hosts only) - * - static_template (deterministic snippet substitution) - * - memory_search (Qdrant + GraphRAG retrieval) - * - * Security rules: - * - http_request: only https://, only allowlisted hosts, timeout enforced - * - static_template: no exec/shell, only {{args.X}} and {{env.Y}} substitution - * - memory_search: read-only store access, no mutation - */ -export class ToolExecutorEngine { - private allowedHosts: string[]; - private vectorStore?: VectorStore; - private entityStore?: EntityStore; - - constructor( - allowedHosts?: string[], - stores?: { vectorStore?: VectorStore; entityStore?: EntityStore }, - ) { - // Default: block everything except documented demo/test hosts - this.allowedHosts = allowedHosts ?? [ - "api.github.com", - "httpbin.org", - "jsonplaceholder.typicode.com", - ]; - if (stores) { - this.vectorStore = stores.vectorStore; - this.entityStore = stores.entityStore; - } - } - - /** - * Execute a single tool call. - */ - async execute( - spec: ToolSpec, - toolCallId: string, - args: Record, - ): Promise { - try { - switch (spec.executor.type) { - case "http_request": - return await this.executeHttp(spec, toolCallId, args); - case "static_template": - return this.executeTemplate(spec, toolCallId, args); - case "memory_search": - return await this.executeMemorySearch(spec, toolCallId, args); - default: - return { - toolCallId, - name: spec.name, - content: `Unknown executor type`, - success: false, - error: `Unknown executor type: ${(spec.executor as ToolExecutor).type}`, - }; - } - } catch (err) { - return { - toolCallId, - name: spec.name, - content: `Error: ${(err as Error).message}`, - success: false, - error: (err as Error).message, - }; - } - } - - /** - * Execute multiple tool calls in parallel (no cross-call dependencies for MVP). - */ - async executeBatch( - specs: ToolSpec[], - toolCalls: Array<{ id: string; name: string; args: Record }>, - ): Promise { - const specMap = new Map(specs.map((s) => [s.name, s])); - return Promise.all( - toolCalls.map((tc) => { - const spec = specMap.get(tc.name); - if (!spec) { - return Promise.resolve({ - toolCallId: tc.id, - name: tc.name, - content: `Unknown tool: ${tc.name}`, - success: false, - error: `Tool not found in registry: ${tc.name}`, - }); - } - return this.execute(spec, tc.id, tc.args); - }), - ); - } - - // ── Private ──────────────────────────────────────────────────── - - private async executeHttp( - spec: ToolSpec, - toolCallId: string, - args: Record, - ): Promise { - const executor = spec.executor as Extract; - const url = new URL(this.substitute(executor.url, args)); - - // Security: enforce https and allowlisted hosts - if (url.protocol !== "https:") { - // Allow httpbin.org on http for testing in dev - if (url.hostname !== "httpbin.org" && url.hostname !== "localhost") { - return { - toolCallId, - name: spec.name, - content: "Only HTTPS URLs are allowed", - success: false, - error: "Non-HTTPS URL blocked", - }; - } - } - - if (!this.allowedHosts.includes(url.hostname) && url.hostname !== "localhost") { - return { - toolCallId, - name: spec.name, - content: `Host not allowlisted: ${url.hostname}`, - success: false, - error: `Host not in allowlist`, - }; - } - - // Build query params from spec - if (executor.query) { - for (const [key, value] of Object.entries(executor.query)) { - url.searchParams.set(key, this.substitute(value, args)); - } - } - - // Build headers - const headers: Record = { - ...(executor.headers ? this.substituteObject(executor.headers, args) : {}), - }; - - const timeoutMs = executor.timeoutMs ?? 10000; - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetch(url.toString(), { - method: executor.method, - headers, - body: executor.body ? JSON.stringify(this.substituteObject(executor.body as Record, args)) : undefined, - signal: controller.signal, - }); - - const body = await response.text(); - const truncated = body.length > 4000 ? body.slice(0, 4000) + "\n... [truncated]" : body; - - return { - toolCallId, - name: spec.name, - content: truncated, - success: response.ok, - error: response.ok ? undefined : `HTTP ${response.status}`, - }; - } finally { - clearTimeout(timeout); - } - } - - private executeTemplate( - spec: ToolSpec, - toolCallId: string, - args: Record, - ): ToolExecutionResult { - const executor = spec.executor as Extract; - const content = this.substitute(executor.template, args); - - return { - toolCallId, - name: spec.name, - content, - success: true, - }; - } - - // ── memory_search executor ──────────────────────────────────────────── - - private async executeMemorySearch( - spec: ToolSpec, - toolCallId: string, - args: Record, - ): Promise { - const executor = spec.executor as Extract; - const query = args.query as string | undefined; - if (!query) { - return { - toolCallId, - name: spec.name, - content: "Error: missing required argument 'query'", - success: false, - error: "Missing required argument 'query'", - }; - } - - const limit = (args.limit as number | undefined) ?? executor.defaultLimit ?? 5; - const tenantId = (args.tenant_id as string | undefined) ?? "default"; - const projectId = args.project_id as string | undefined; - - const parts: string[] = []; - - // Qdrant semantic search - if (this.vectorStore) { - try { - const results = await this.vectorStore.search(query, { - limit, - tenantId, - projectId, - }); - if (results.length > 0) { - const filtered = executor.scoreThreshold - ? results.filter((r) => r.score >= executor.scoreThreshold!) - : results; - if (filtered.length > 0) { - parts.push("=== Semantic Memory (Qdrant) ==="); - for (const r of filtered) { - parts.push(`[relevance=${r.score.toFixed(2)}] ${r.payload.text}`); - } - } - } - } catch (err) { - parts.push(`[Qdrant search failed: ${(err as Error).message}]`); - } - } - - // GraphRAG entity search - if (this.entityStore) { - try { - const graphResults = await this.entityStore.searchGraph(query, tenantId, { - limit, - projectId, - }); - if (graphResults.length > 0) { - parts.push("=== Entity Graph (GraphRAG) ==="); - const lines = this.entityStore.formatContext(graphResults); - parts.push(...lines); - } - } catch (err) { - parts.push(`[Graph search failed: ${(err as Error).message}]`); - } - } - - if (parts.length === 0) { - return { - toolCallId, - name: spec.name, - content: "No relevant memories found.", - success: true, - }; - } - - return { - toolCallId, - name: spec.name, - content: parts.join("\n\n"), - success: true, - }; - } - - /** - * Substitute {{args.xxx}} and {{env.YYY}} placeholders in a string. - * Special: {{env.NOW}} returns the current time in ISO 8601. - */ - private substitute(template: string, args: Record): string { - return template.replace(/\{\{(args|env)\.([\w.]+)\}\}/g, (match, type, key) => { - if (type === "args") { - return String(args[key] ?? ""); - } - if (type === "env") { - if (key === "NOW") return new Date().toISOString(); - return process.env[key] ?? ""; - } - return match; - }); - } - - /** - * Substitute placeholders in all values of an object. - */ - private substituteObject( - obj: Record, - args: Record, - ): Record { - const result: Record = {}; - for (const [key, value] of Object.entries(obj)) { - result[key] = this.substitute(value, args); - } - return result; - } -} \ No newline at end of file diff --git a/rag-gateway/src/tool/registry.ts b/rag-gateway/src/tool/registry.ts deleted file mode 100644 index 44e4b6b..0000000 --- a/rag-gateway/src/tool/registry.ts +++ /dev/null @@ -1,180 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import type { ToolSpec, ToolRecord, ToolStatus } from "./types.js"; -import crypto from "node:crypto"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -/** - * Check if a model name matches a pattern (supports glob: "claude-*", or exact match). - */ -function modelMatches(model: string, pattern: string): boolean { - if (pattern === "*") return true; - if (pattern.endsWith("-*")) { - return model.startsWith(pattern.slice(0, -2)); - } - return model === pattern; -} - -/** - * ToolRegistry loads declarative tool specs from a JSON file and - * provides filtered views for injection into requests. - * - * In later iterations, this will also load from PostgreSQL. - */ -export class ToolRegistry { - private tools: Map = new Map(); - - constructor() { } - - /** - * Load tool specs from a JSON file. - * File format: { "tools": ToolSpec[] } or ToolSpec[]. - */ - loadFromFile(filePath?: string): void { - const resolvedPath = filePath ?? path.resolve(__dirname, "../../tools.json"); - if (!fs.existsSync(resolvedPath)) { - console.log(`⚠️ Tool registry file not found: ${resolvedPath} — no tools loaded`); - return; - } - - const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")); - const specs: ToolSpec[] = Array.isArray(raw) ? raw : raw.tools; - - if (!Array.isArray(specs)) { - console.warn("⚠️ tools.json has unexpected format — expected array or { tools: [] }"); - return; - } - - for (const spec of specs) { - if (!spec.name || !spec.executor) { - console.warn(`⚠️ Skipping invalid tool spec: missing name or executor`); - continue; - } - const record: ToolRecord = { - ...spec, - id: crypto.randomUUID(), - status: "active", - createdAt: new Date().toISOString(), - }; - this.tools.set(spec.name, record); - } - - console.log(`🔧 Tool registry loaded: ${this.tools.size} tools`); - } - - /** - * Register a single tool spec programmatically. - */ - register(spec: ToolSpec): void { - const record: ToolRecord = { - ...spec, - id: crypto.randomUUID(), - status: "active", - createdAt: new Date().toISOString(), - }; - this.tools.set(spec.name, record); - } - - /** - * Get all active tools, optionally filtered by model compatibility. - */ - getActiveTools(model?: string): ToolSpec[] { - const results: ToolSpec[] = []; - for (const tool of this.tools.values()) { - if (tool.status !== "active") continue; - if (tool.internal) continue; - if (model && !this.shouldInjectTool(tool, model)) continue; - results.push(tool); - } - return results; - } - - /** - * Check whether a tool should be injected for a given model. - * Blocklist (disableForModels) is checked first; then allowlist (compatibleModels). - * No allowlist = available to all unblocked models. - */ - shouldInjectTool(spec: ToolSpec, model: string): boolean { - // Blocklist check — if any pattern matches, exclude immediately - if (spec.disableForModels) { - for (const pattern of spec.disableForModels) { - if (modelMatches(model, pattern)) return false; - } - } - - // Allowlist: if compatibleModels exists, model must match one - if (spec.compatibleModels && spec.compatibleModels.length > 0) { - for (const pattern of spec.compatibleModels) { - if (modelMatches(model, pattern)) return true; - } - return false; - } - - // No allowlist = available to all models (that aren't blocked) - return true; - } - - /** - * Resolve a per-model tool_choice policy by checking all active tools' - * toolChoicePolicy maps. First matching policy wins. - */ - resolveToolChoicePolicy( - model: string, - ): "auto" | "any" | "none" | "required" | undefined { - for (const tool of this.tools.values()) { - if (tool.status !== "active") continue; - if (!tool.toolChoicePolicy) continue; - for (const [pattern, choice] of Object.entries(tool.toolChoicePolicy)) { - if (modelMatches(model, pattern)) return choice; - } - } - return undefined; - } - - /** - * Get a tool spec by name. - */ - get(name: string): ToolSpec | undefined { - return this.tools.get(name); - } - - /** - * Convert ToolSpec to OpenAI-compatible tool definition. - */ - toOpenAITool(spec: ToolSpec): { - type: "function"; - function: { - name: string; - description: string; - parameters: Record; - }; - } { - return { - type: "function", - function: { - name: spec.name, - description: spec.description, - parameters: spec.input_schema as unknown as Record, - }, - }; - } - - /** - * Convert all active tools to OpenAI-compatible format. - */ - toOpenAITools(model?: string): Array<{ - type: "function"; - function: { name: string; description: string; parameters: Record }; - }> { - return this.getActiveTools(model).map((t) => this.toOpenAITool(t)); - } - - /** - * Total count of registered tools. - */ - get count(): number { - return this.tools.size; - } -} \ No newline at end of file diff --git a/rag-gateway/src/tool/simplify.ts b/rag-gateway/src/tool/simplify.ts deleted file mode 100644 index 6f38397..0000000 --- a/rag-gateway/src/tool/simplify.ts +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Schema simplification utility for models that struggle with complex JSON schemas. - * - * Given a ToolSpec and a model name, returns a simplified input_schema if the - * model is in the spec's schemaSimplifyFor list. Simplification strategies: - * - * 1. Strip lengthy descriptions from properties (keep param names and types only) - * 2. Change "array" type items to "string" if the items schema is deeply nested - * 3. Cap `required` to the first 5 fields - * 4. Flatten nested object properties to `"type": "string"` if depth > 2 - */ - -import type { ToolSpec } from "./types.js"; - -/** - * Return the input_schema for a tool, simplified if the model is in the - * spec's schemaSimplifyFor list. - */ -export function getSimplifiedSchema( - spec: ToolSpec, - model: string, -): ToolSpec["input_schema"] { - const shouldSimplify = - spec.schemaSimplifyFor && - spec.schemaSimplifyFor.some((p) => modelMatchesSimple(model, p)); - - if (!shouldSimplify) return spec.input_schema; - - return simplifySchema(spec.input_schema); -} - -function modelMatchesSimple(model: string, pattern: string): boolean { - if (pattern === "*") return true; - if (pattern.endsWith("-*")) return model.startsWith(pattern.slice(0, -2)); - return model === pattern; -} - -function simplifySchema(input: { - type: "object"; - properties: Record; - required?: string[]; -}): { - type: "object"; - properties: Record; - required?: string[]; -} { - const simplified: Record = {}; - - for (const [key, value] of Object.entries(input.properties)) { - if (typeof value === "object" && value !== null) { - const prop = value as Record; - - if (prop.type === "object") { - // Flatten nested objects — only keep type - simplified[key] = { type: "object", properties: {}, description: simplifyDescription(prop.description) }; - } else if (prop.type === "array") { - // Simplify array items - const items = prop.items as Record | undefined; - if (items && typeof items === "object" && items.type === "object") { - simplified[key] = { type: "array", items: { type: "string" }, description: simplifyDescription(prop.description) }; - } else { - simplified[key] = { type: "array", description: simplifyDescription(prop.description) }; - } - } else { - // Keep scalar types, strip verbose descriptions - simplified[key] = { - type: prop.type ?? "string", - description: simplifyDescription(prop.description), - ...(prop.enum ? { enum: prop.enum } : {}), - }; - } - } else { - simplified[key] = value; - } - } - - const required = input.required ? input.required.slice(0, 5) : undefined; - - return { - type: "object", - properties: simplified, - required, - }; -} - -function simplifyDescription( - desc: unknown, -): string | undefined { - if (typeof desc !== "string") return undefined; - // Keep only the first sentence (up to 80 chars) - const trimmed = desc.split("\n")[0].trim(); - if (trimmed.length <= 80) return trimmed; - return trimmed.slice(0, 77) + "..."; -} \ No newline at end of file diff --git a/rag-gateway/src/tool/types.ts b/rag-gateway/src/tool/types.ts deleted file mode 100644 index 51a43b1..0000000 --- a/rag-gateway/src/tool/types.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Declarative tool spec — see docs/universal-rag-gateway.md §Declarative Tool Runtime. - * - * Tool specs are data, not code. Each executor type is a well-known capability - * with its own security model. - */ - -// ── Executor types ──────────────────────────────────────────────── - -export interface HttpRequestExecutor { - type: "http_request"; - method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; - url: string; - headers?: Record; - query?: Record; - body?: unknown; - /** Timeout in ms (default 10000). Exceeding this fails the tool call. */ - timeoutMs?: number; -} - -export interface StaticTemplateExecutor { - type: "static_template"; - /** Template string with {{args.xxx}} and {{env.YYY}} placeholders. */ - template: string; - /** Content-Type of the output */ - contentType?: string; -} - -export interface MemorySearchExecutor { - type: "memory_search"; - /** Default max results to return (overridable by args.limit) */ - defaultLimit?: number; - /** Minimum relevance score threshold (0-1) */ - scoreThreshold?: number; -} - -export type ToolExecutor = HttpRequestExecutor | StaticTemplateExecutor | MemorySearchExecutor; - -// ── Tool spec ───────────────────────────────────────────────────── - -export interface ToolSpec { - name: string; - description: string; - input_schema: { - type: "object"; - properties: Record; - required?: string[]; - }; - executor: ToolExecutor; - /** Per-model compatibility allowlist. Omit = available to all models. */ - compatibleModels?: string[]; - /** Per-model blocklist. If the model matches, the tool is excluded. */ - disableForModels?: string[]; - /** Per-model default tool_choice policy keyed by model name/glob. */ - toolChoicePolicy?: Record; - /** Models that should receive a simplified input_schema. */ - schemaSimplifyFor?: string[]; - /** If true, this tool is only injected when the request explicitly opts in. */ - internal?: boolean; -} - -// ── Registry state ──────────────────────────────────────────────── - -export type ToolStatus = "active" | "candidate" | "retired"; - -export interface ToolRecord extends ToolSpec { - id: string; - status: ToolStatus; - createdAt: string; -} \ No newline at end of file diff --git a/rag-gateway/src/types.ts b/rag-gateway/src/types.ts deleted file mode 100644 index 75f24f1..0000000 --- a/rag-gateway/src/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Persisted types for the gateway — graph entities, relationships, extraction. - */ - -// ─── Graph Entity/Relation types (GraphRAG) ───────────────────────── - -export interface GraphEntity { - id: string; - tenantId: string; - projectId?: string; - type: string; - name: string; - properties: Record; - createdAt: Date; -} - -export interface GraphRelation { - id: string; - tenantId: string; - projectId?: string; - fromEntityId: string; - toEntityId: string; - relation: string; - properties: Record; - createdAt: Date; -} - -/** - * Result of a graph search: matched entity plus its 1-2 hop neighborhood. - */ -export interface GraphSearchResult { - entity: GraphEntity; - relations: Array<{ - relation: GraphRelation; - fromEntity: GraphEntity; - toEntity: GraphEntity; - }>; -} - -/** - * Structured output from the LLM extraction prompt. - */ -export interface ExtractionResult { - entities: Array<{ - type: string; - name: string; - properties: Record; - }>; - relationships: Array<{ - from: string; - to: string; - relation: string; - properties: Record; - }>; -} \ No newline at end of file diff --git a/rag-gateway/src/vector/embedding.ts b/rag-gateway/src/vector/embedding.ts deleted file mode 100644 index 959eb6d..0000000 --- a/rag-gateway/src/vector/embedding.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { GatewayConfig } from "../config.js"; - -/** - * Embedding service that converts text to vectors for Qdrant search. - * - * Uses native fetch instead of the OpenAI SDK because the SDK misreports - * bge-m3 vector dimensions (returns 256-dim zeros instead of 1024-dim real). - * - * Configured via EMBEDDING_API_KEY / EMBEDDING_BASE_URL / EMBEDDING_MODEL. - */ -export class EmbeddingService { - private apiKey: string; - private baseUrl: string; - private model: string; - - constructor(config: GatewayConfig, model?: string) { - this.apiKey = config.embeddingApiKey; - this.baseUrl = config.embeddingBaseUrl.replace(/\/+$/, ""); - this.model = model ?? config.embeddingModel; - } - - /** - * Generate an embedding vector for a single text string. - */ - async embed(text: string): Promise { - const results = await this._create(text); - return results[0]; - } - - /** - * Generate embedding vectors for multiple texts (batched). - */ - async embedBatch(texts: string[]): Promise { - if (texts.length === 0) return []; - return this._create(texts); - } - - /** - * Internal: call the OpenAI-compatible embeddings API via native fetch. - */ - private async _create(input: string | string[]): Promise { - const url = `${this.baseUrl}/embeddings`; - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${this.apiKey}`, - }, - body: JSON.stringify({ - model: this.model, - input, - }), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Embedding API ${response.status}: ${text}`); - } - - const json = (await response.json()) as { - data: Array<{ embedding: number[]; index: number }>; - }; - - // Sort by index to maintain order - return json.data - .sort((a, b) => a.index - b.index) - .map((d) => d.embedding); - } -} \ No newline at end of file diff --git a/rag-gateway/src/vector/qdrant.ts b/rag-gateway/src/vector/qdrant.ts deleted file mode 100644 index 54cc2b8..0000000 --- a/rag-gateway/src/vector/qdrant.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * Minimal Qdrant REST client using native fetch. - * - * Replaces @qdrant/js-client-rest which is incompatible with Node.js 26's undici v8. - * Implements only the subset of the Qdrant API needed by the gateway: - * - createCollection (if not exists) - * - upsert points - * - search - * - getCollections - * - deleteCollection - */ - -const DEFAULT_COLLECTION = "memory_chunks"; - -export interface QdrantStoreOptions { - /** Override collection name (default: "memory_chunks") */ - collectionName?: string; - /** Override vector dimension (default: 1024 for bge-m3) */ - vectorSize?: number; -} - -export interface QdrantSearchResult { - id: string; - score: number; - payload: { - text: string; - tenantId: string; - projectId?: string; - sourceMessageId?: string; - kind: string; - createdAt: string; - }; -} - -interface QdrantPoint { - id: string; - vector: number[]; - payload: Record; -} - -export class QdrantStore { - private baseUrl: string; - private collectionName: string; - private vectorSize: number; - - constructor(url: string, options?: QdrantStoreOptions) { - // Strip trailing slash from base URL - this.baseUrl = url.replace(/\/+$/, ""); - this.collectionName = options?.collectionName ?? DEFAULT_COLLECTION; - this.vectorSize = options?.vectorSize ?? 1024; // bge-m3 default - } - - private async request( - method: string, - path: string, - body?: unknown, - ): Promise { - const url = `${this.baseUrl}${path}`; - const res = await fetch(url, { - method, - headers: { "Content-Type": "application/json" }, - body: body ? JSON.stringify(body) : undefined, - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`Qdrant ${method} ${path}: ${res.status} ${text}`); - } - const json = (await res.json()) as { result: T; status: string }; - return json.result; - } - - /** - * Ensure the collection exists (create if missing). Idempotent. - */ - async ensureCollection(): Promise { - const result = await this.request<{ collections: { name: string }[] }>("GET", "/collections"); - const exists = result.collections.some((c) => c.name === this.collectionName); - - if (!exists) { - await this.request("PUT", `/collections/${this.collectionName}`, { - vectors: { - size: this.vectorSize, - distance: "Cosine", - }, - }); - console.log( - `📦 Qdrant collection "${this.collectionName}" created (size=${this.vectorSize})`, - ); - } - } - - /** - * Upsert a single point (vector + payload). - */ - async upsert( - id: string, - vector: number[], - payload: Record, - ): Promise { - await this.upsertBatch([{ id, vector, payload }]); - } - - /** - * Upsert multiple points in one batch. - */ - async upsertBatch(points: QdrantPoint[]): Promise { - if (points.length === 0) return; - await this.request("PUT", `/collections/${this.collectionName}/points`, { - points: points.map((p) => ({ - id: p.id, - vector: p.vector, - payload: p.payload, - })), - }); - } - - /** - * Ensure payload indexes exist for filterable fields (tenantId, projectId). - * - * Without these, Qdrant does a full scan of all points when filtering, - * which degrades to O(n) per search. - */ - async ensurePayloadIndexes(): Promise { - const indexDefs = [ - { field_name: "tenantId", field_type: "keyword" }, - { field_name: "projectId", field_type: "keyword" }, - ]; - for (const idx of indexDefs) { - try { - await this.request("PUT", `/collections/${this.collectionName}/index`, idx); - console.log(`📦 Qdrant payload index "${idx.field_name}" created`); - } catch { - // Index already exists — fine - } - } - } - - /** - * Search for similar vectors by query vector. - */ - async search( - vector: number[], - options?: { - limit?: number; - tenantId?: string; - projectId?: string; - }, - ): Promise { - const must: Array> = []; - if (options?.tenantId) { - must.push({ - key: "tenantId", - match: { value: options.tenantId }, - }); - } - if (options?.projectId) { - must.push({ - key: "projectId", - match: { value: options.projectId }, - }); - } - const filter = must.length > 0 ? { must } : undefined; - - const result = await this.request<{ - id: string; - score: number; - payload: QdrantSearchResult["payload"]; - }[]>("POST", `/collections/${this.collectionName}/points/search`, { - vector, - limit: options?.limit ?? 5, - filter, - with_payload: true, - }); - - return result.map((r) => ({ - id: String(r.id), - score: r.score ?? 0, - payload: r.payload, - })); - } - - /** - * Delete a collection (for testing / reset). - */ - async deleteCollection(): Promise { - await this.request("DELETE", `/collections/${this.collectionName}`); - } -} \ No newline at end of file diff --git a/rag-gateway/src/vector/store.ts b/rag-gateway/src/vector/store.ts deleted file mode 100644 index e03fe1e..0000000 --- a/rag-gateway/src/vector/store.ts +++ /dev/null @@ -1,162 +0,0 @@ -import crypto from "node:crypto"; -import type { EmbeddingService } from "./embedding.js"; -import type { QdrantStore, QdrantSearchResult } from "./qdrant.js"; - -/** - * A chunk of a message ready for vector storage. - */ -export interface ChunkedMessage { - id: string; - text: string; - kind: "user_message" | "assistant_message" | "system_prompt" | "summary"; - tenantId: string; - projectId?: string; - sourceMessageId: string; - conversationId: string; -} - -/** - * High-level store layer that embeds text chunks and indexes them in Qdrant. - */ -export class VectorStore { - private embedding: EmbeddingService; - private qdrant: QdrantStore; - - constructor(embedding: EmbeddingService, qdrant: QdrantStore) { - this.embedding = embedding; - this.qdrant = qdrant; - } - - /** - * Chunk a message into segments (simple split by newline or by token-budget). - * For MVP: split on double-newlines, fall back to 500-char segments. - */ - chunkMessage( - message: ChunkedMessage, - maxChunkLength = 500, - ): Array<{ text: string; id: string }> { - const segments: Array<{ text: string; id: string }> = []; - - // Try splitting by double-newline first (paragraph boundaries) - const paragraphs = message.text.split(/\n\n+/); - - for (const para of paragraphs) { - const trimmed = para.trim(); - if (!trimmed) continue; - - if (trimmed.length <= maxChunkLength) { - segments.push({ - id: crypto.randomUUID(), - text: trimmed, - }); - } else { - // Further split long paragraphs by sentence or char boundary - let remaining = trimmed; - while (remaining.length > 0) { - const chunk = remaining.slice(0, maxChunkLength); - segments.push({ - id: crypto.randomUUID(), - text: chunk, - }); - remaining = remaining.slice(maxChunkLength); - } - } - } - - return segments; - } - - /** - * Index a single message: chunk → embed → upsert into Qdrant. - */ - async indexMessage(chunked: ChunkedMessage): Promise { - const segments = this.chunkMessage(chunked); - if (segments.length === 0) return; - - // Generate embeddings in batch - const vectors = await this.embedding.embedBatch( - segments.map((s) => s.text), - ); - - await this.qdrant.upsertBatch( - segments.map((seg, i) => ({ - id: seg.id, - vector: vectors[i], - payload: { - text: seg.text, - tenantId: chunked.tenantId, - projectId: chunked.projectId ?? null, - sourceMessageId: chunked.sourceMessageId, - conversationId: chunked.conversationId, - kind: chunked.kind, - createdAt: new Date().toISOString(), - }, - })), - ); - } - - /** - * Index multiple messages in batch. - */ - async indexMessages(chunks: ChunkedMessage[]): Promise { - // Flatten all segments across all messages - const allSegments: Array<{ - text: string; - id: string; - payload: Record; - }> = []; - - for (const chunk of chunks) { - const segments = this.chunkMessage(chunk); - for (const seg of segments) { - allSegments.push({ - id: seg.id, - text: seg.text, - payload: { - text: seg.text, - tenantId: chunk.tenantId, - projectId: chunk.projectId ?? null, - sourceMessageId: chunk.sourceMessageId, - conversationId: chunk.conversationId, - kind: chunk.kind, - createdAt: new Date().toISOString(), - }, - }); - } - } - - if (allSegments.length === 0) return; - - // Batch embed all texts - const vectors = await this.embedding.embedBatch( - allSegments.map((s) => s.text), - ); - - await this.qdrant.upsertBatch( - allSegments.map((seg, i) => ({ - id: seg.id, - vector: vectors[i], - payload: seg.payload, - })), - ); - } - - /** - * Search for relevant memory chunks by query text. - */ - async search( - query: string, - options?: { - limit?: number; - tenantId?: string; - projectId?: string; - }, - ): Promise { - const vector = await this.embedding.embed(query); - return this.qdrant.search(vector, { - limit: options?.limit ?? 5, - tenantId: options?.tenantId, - projectId: options?.projectId, - }); - } -} \ No newline at end of file diff --git a/rag-gateway/tests/helpers/fixtures.ts b/rag-gateway/tests/helpers/fixtures.ts deleted file mode 100644 index 2df8157..0000000 --- a/rag-gateway/tests/helpers/fixtures.ts +++ /dev/null @@ -1,252 +0,0 @@ -import type { - NormalizedMessage, - NormalizedTool, - ToolChoice, - ContextBudget, - ExtractionResult, - GraphEntity, - GraphRelation, - GatewayRequest, - RequestTelemetry, -} from "../../src/types.js"; -import type { ToolSpec, HttpRequestExecutor, StaticTemplateExecutor } from "../../src/tool/types.js"; -import type { BudgetProfile, RetrievalConstraints } from "../../src/planner/budget.js"; - -// ── Messages ──────────────────────────────────────────────────────────── - -export const systemMessage: NormalizedMessage = { - role: "system", - content: "You are a helpful assistant.", -}; - -export const userMessage: NormalizedMessage = { - role: "user", - content: "Hello, how are you?", -}; - -export const assistantMessage: NormalizedMessage = { - role: "assistant", - content: "I'm doing well, thank you!", -}; - -export const toolResultMessage: NormalizedMessage = { - role: "tool", - content: '{"result": "success"}', - tool_call_id: "call_123", -}; - -export const assistantWithToolCalls: NormalizedMessage = { - role: "assistant", - content: "Let me look that up...", - tool_calls: [ - { - id: "call_abc", - type: "function", - function: { name: "get_current_time", arguments: "{}" }, - }, - ], -}; - -export const multiParagraphMessage: NormalizedMessage = { - role: "user", - content: "This is paragraph one.\n\nThis is paragraph two.\n\nThis is paragraph three, which is intentionally kept long to test chunking behavior at boundaries.", -}; - -export const contentPartMessage: NormalizedMessage = { - role: "user", - content: [ - { type: "text", text: "What's in this image?" }, - { type: "image_url", image_url: { url: "https://example.com/image.png", detail: "auto" } }, - ], -}; - -// ── Intent-focused messages ───────────────────────────────────────────── - -export const debugMessage: NormalizedMessage = { - role: "user", - content: "I'm getting an error when I try to load the page. The stack trace shows a null pointer exception. Can you help me debug this?", -}; - -export const codingEditMessage: NormalizedMessage = { - role: "user", - content: "Please implement a new function that handles user authentication. I need to refactor the login module.", -}; - -export const designMessage: NormalizedMessage = { - role: "user", - content: "I'm designing a microservices architecture for our platform. Should we use event-driven or request-driven communication between services?", -}; - -export const qaMessage: NormalizedMessage = { - role: "user", - content: "What is the difference between SQL and NoSQL databases? Can you explain how indexing works?", -}; - -export const shortMessage: NormalizedMessage = { - role: "user", - content: "Hi", -}; - -export const longMessage: NormalizedMessage = { - role: "user", - content: "A".repeat(2500), -}; - -// ── Tool specs ────────────────────────────────────────────────────────── - -export const httpToolSpec: ToolSpec = { - name: "lookup_http_status", - description: "Look up the meaning of an HTTP status code.", - input_schema: { - type: "object", - properties: { - code: { type: "string", description: "HTTP status code" }, - }, - required: ["code"], - }, - executor: { - type: "http_request", - method: "GET", - url: "https://httpbin.org/status/{{args.code}}", - timeoutMs: 5000, - } as HttpRequestExecutor, -}; - -export const templateToolSpec: ToolSpec = { - name: "get_current_time", - description: "Get the current date and time.", - input_schema: { - type: "object", - properties: {}, - required: [], - }, - executor: { - type: "static_template", - template: "Current time: {{env.NOW}}", - } as StaticTemplateExecutor, -}; - -export const modelFilteredTool: ToolSpec = { - name: "premium_analyzer", - description: "Premium analysis tool for capable models only.", - input_schema: { - type: "object", - properties: { - data: { type: "string" }, - }, - required: ["data"], - }, - executor: { - type: "static_template", - template: "Analysis: {{args.data}}", - } as StaticTemplateExecutor, - compatibleModels: ["claude-*", "gpt-4*"], - disableForModels: ["gpt-3.5*"], - schemaSimplifyFor: ["claude-3-haiku*"], -}; - -// ── Budget profiles ───────────────────────────────────────────────────── - -export const budgetProfileDefaults: Record = { - "coding-edit": { maxContextTokens: 16000, staticPrefixTokens: 4000, ragTokens: 2000, recentHistoryTokens: 6000, toolSchemaTokens: 2000 }, - debug: { maxContextTokens: 24000, staticPrefixTokens: 4000, ragTokens: 8000, recentHistoryTokens: 6000, toolSchemaTokens: 3000 }, - design: { maxContextTokens: 32000, staticPrefixTokens: 6000, ragTokens: 12000, recentHistoryTokens: 8000, toolSchemaTokens: 3000 }, - qa: { maxContextTokens: 20000, staticPrefixTokens: 4000, ragTokens: 6000, recentHistoryTokens: 5000, toolSchemaTokens: 3000 }, -}; - -// ── Entity fixtures ───────────────────────────────────────────────────── - -export const mockExtractionResult: ExtractionResult = { - entities: [ - { type: "project", name: "MyApp", properties: { language: "TypeScript" } }, - { type: "database", name: "PostgreSQL", properties: { version: "16" } }, - { type: "technology", name: "Redis", properties: { purpose: "caching" } }, - ], - relationships: [ - { from: "MyApp", to: "PostgreSQL", relation: "uses", properties: {} }, - { from: "MyApp", to: "Redis", relation: "uses", properties: { context: "caching" } }, - ], -}; - -export const mockGraphEntity: GraphEntity = { - id: "entity-1", - tenantId: "default", - type: "project", - name: "MyApp", - properties: { language: "TypeScript" }, - createdAt: new Date(), -}; - -export const mockGraphRelation: GraphRelation = { - id: "edge-1", - tenantId: "default", - fromEntityId: "entity-1", - toEntityId: "entity-2", - relation: "uses", - properties: {}, - createdAt: new Date(), -}; - -// ── OpenAI-style request/response ─────────────────────────────────────── - -export const openaiRequestBody = { - model: "gpt-4o", - messages: [ - { role: "system", content: "You are a helpful assistant." }, - { role: "user", content: "Hello!" }, - ], - stream: false, -}; - -export const openaiChatResponse = { - id: "chatcmpl-123", - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: "gpt-4o", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: "Hello! How can I help you today?", - }, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: 25, - completion_tokens: 10, - total_tokens: 35, - }, -}; - -// ── Telemetry ─────────────────────────────────────────────────────────── - -export const mockTelemetry: RequestTelemetry = { - requestId: "req-123", - tenantId: "default", - client: "test", - model: "gpt-4o", - messageCount: 2, - toolCount: 0, - injectedTools: 0, - toolCallsExecuted: 0, - retrievalHits: 0, - stream: false, - providerLatencyMs: 100, - skippedComponents: [], - createdAt: new Date(), -}; - -// ── MCP ───────────────────────────────────────────────────────────────── - -export const mockMcpServerConfigStdio = { - command: "npx", - args: ["-y", "@modelcontextprotocol/server-filesystem"], - env: { DEBUG: "true" }, -}; - -export const mockMcpServerConfigUrl = { - url: "https://example.com/mcp", - headers: { Authorization: "Bearer test-token" }, -}; \ No newline at end of file diff --git a/rag-gateway/tests/helpers/mock-server.ts b/rag-gateway/tests/helpers/mock-server.ts deleted file mode 100644 index ca0b527..0000000 --- a/rag-gateway/tests/helpers/mock-server.ts +++ /dev/null @@ -1,265 +0,0 @@ -import http from "node:http"; - -export interface RequestRecord { - timestamp: string; - method: string; - path: string; - body: any; - inputCharCount: number; - inputTokenEstimate: number; -} - -/** - * A mock HTTP server that records every request payload for token comparison. - * Always returns a fixed success response. - */ -export class MockProviderServer { - private server: http.Server; - private _port: number = 0; - private _recordedRequests: RequestRecord[] = []; - private _responseOverrides: Array<{ - matchBody?: (body: any) => boolean; - response: any; - }> = []; - - constructor() { - this.server = http.createServer((req, res) => { - // Handle CORS preflight - if (req.method === "OPTIONS") { - res.writeHead(200, { "Access-Control-Allow-Origin": "*" }); - res.end(); - return; - } - - const chunks: Buffer[] = []; - req.on("data", (chunk: Buffer) => chunks.push(chunk)); - req.on("end", () => { - const bodyStr = Buffer.concat(chunks).toString("utf-8"); - let body: any; - try { - body = JSON.parse(bodyStr); - } catch { - body = { raw: bodyStr }; - } - - const inputCharCount = this.countInputChars(body); - const inputTokenEstimate = Math.ceil(inputCharCount / 4); - - this._recordedRequests.push({ - timestamp: new Date().toISOString(), - method: req.method ?? "UNKNOWN", - path: req.url ?? "/", - body, - inputCharCount, - inputTokenEstimate, - }); - - // Check for override response - const override = this._responseOverrides.find((o) => - o.matchBody ? o.matchBody(body) : false, - ); - - const response = override?.response ?? { - id: "chatcmpl-mock", - object: "chat.completion", - created: Math.floor(Date.now() / 1000), - model: body?.model ?? "gpt-4o", - choices: [ - { - index: 0, - message: { - role: "assistant", - content: "This is a simulated assistant response for testing purposes.", - }, - finish_reason: "stop", - }, - ], - usage: { - prompt_tokens: inputTokenEstimate, - completion_tokens: 50, - total_tokens: inputTokenEstimate + 50, - }, - }; - - const responseBody = JSON.stringify(response); - res.writeHead(200, { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": "*", - }); - res.end(responseBody); - }); - }); - } - - private countInputChars(body: any): number { - if (!body || !body.messages) return 0; - return body.messages.reduce((sum: number, m: any) => { - const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""); - return sum + content.length + 20; // +20 for role markers - }, 0); - } - - /** - * Set a response override for a specific request. - * Useful for simulating tool call responses during roundtrip tests. - */ - setResponseOverride( - matchFn: (body: any) => boolean, - response: any, - ): void { - this._responseOverrides.push({ matchBody: matchFn, response }); - } - - get port(): number { - return this._port; - } - - get recordedRequests(): RequestRecord[] { - return [...this._recordedRequests]; - } - - get lastRequest(): RequestRecord | undefined { - return this._recordedRequests[this._recordedRequests.length - 1]; - } - - reset(): void { - this._recordedRequests = []; - this._responseOverrides = []; - } - - start(): Promise { - return new Promise((resolve) => { - this.server.listen(0, "127.0.0.1", () => { - const addr = this.server.address(); - if (addr && typeof addr === "object") { - this._port = addr.port; - } - resolve(); - }); - }); - } - - stop(): Promise { - return new Promise((resolve) => { - this.server.close(() => resolve()); - }); - } -} - -/** - * In-memory VectorStore mock for integration tests. - * Stores indexed texts and returns them by keyword overlap on search. - */ -export class MockVectorStore { - private chunks: Array<{ text: string; tenantId: string; kind: string }> = []; - - async indexMessage(msg: { - id: string; - text: string; - kind: string; - tenantId: string; - }): Promise { - const segments = this.chunkText(msg.text, 500); - for (const seg of segments) { - this.chunks.push({ - text: seg, - tenantId: msg.tenantId, - kind: msg.kind, - }); - } - } - - async indexMessages(msgs: Array<{ - id: string; - text: string; - kind: string; - tenantId: string; - }>): Promise { - for (const msg of msgs) { - await this.indexMessage(msg); - } - } - - async search( - query: string, - options?: { limit?: number; tenantId?: string }, - ): Promise> { - const queryTokens = query.toLowerCase().split(/\s+/).filter((t) => t.length > 2); - const scored = this.chunks - .filter((c) => !options?.tenantId || c.tenantId === options.tenantId) - .map((c) => { - const textLower = c.text.toLowerCase(); - const matchCount = queryTokens.filter((t) => textLower.includes(t)).length; - // Simulate semantic embedding similarity: only chunks with strong keyword - // overlap (>=25% of query tokens match) score above the 0.55 threshold. - // Real embedding models naturally filter to 2-3 most relevant chunks. - const ratio = queryTokens.length > 0 ? matchCount / queryTokens.length : 0; - const score = matchCount > 0 && ratio >= 0.25 - ? 0.6 + Math.min(ratio, 0.35) - : 0; - return { score, payload: c }; - }) - .filter((r) => r.score > 0) - .sort((a, b) => b.score - a.score) - .slice(0, Math.min(options?.limit ?? 5, 3)); // Cap at 3 — real semantic search rarely returns more relevant chunks - - return scored; - } - - reset(): void { - this.chunks = []; - } - - get chunkCount(): number { - return this.chunks.length; - } - - private chunkText(text: string, maxChunkLength: number): string[] { - const segments: string[] = []; - const paragraphs = text.split(/\n\n+/); - for (const para of paragraphs) { - const trimmed = para.trim(); - if (!trimmed) continue; - if (trimmed.length <= maxChunkLength) { - segments.push(trimmed); - } else { - let remaining = trimmed; - while (remaining.length > 0) { - segments.push(remaining.slice(0, maxChunkLength)); - remaining = remaining.slice(maxChunkLength); - } - } - } - return segments; - } -} - -/** - * Estimate tokens from a text string. - */ -export function estimateTokens(text: string): number { - return Math.ceil(text.length / 4); -} - -/** - * Count total estimated input tokens from a request body. - */ -export function countPayloadTokens(body: any): number { - if (!body || !body.messages) return 0; - return body.messages.reduce((sum: number, m: any) => { - const content = typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""); - return sum + estimateTokens(content) + 5; // +5 for role markers - }, 0); -} - -/** - * Build a conversation history array from sequential messages. - */ -export function buildConversationHistory(messages: string[]): Array<{ role: string; content: string }> { - const history: Array<{ role: string; content: string }> = []; - history.push({ role: "system", content: "You are a helpful assistant." }); - for (let i = 0; i < messages.length; i++) { - history.push({ role: i % 2 === 0 ? "user" : "assistant", content: messages[i] }); - } - return history; -} \ No newline at end of file diff --git a/rag-gateway/tests/setup.ts b/rag-gateway/tests/setup.ts deleted file mode 100644 index d7def22..0000000 --- a/rag-gateway/tests/setup.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { vi } from "vitest"; - -// ── Mock all external SDKs at module level ────────────────────────────── - -// Use `beforeAll` hoisted mock factories — these must NOT reference outer scope `vi` after hoisting -vi.mock("openai", () => { - class MockOpenAI { - apiKey: string; - baseURL: string; - chat: { completions: { create: ReturnType } }; - embeddings: { create: ReturnType }; - - constructor(opts: { apiKey: string; baseURL?: string }) { - this.apiKey = opts.apiKey; - this.baseURL = opts.baseURL ?? "https://api.openai.com/v1"; - this.chat = { - completions: { - create: vi.fn().mockResolvedValue({ - id: "chatcmpl-mock", - model: "gpt-4o", - choices: [{ message: { content: "Mock response" }, finish_reason: "stop" }], - usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 }, - }), - }, - }; - this.embeddings = { - create: vi.fn().mockResolvedValue({ - data: [{ embedding: [0.1, 0.2, 0.3], index: 0 }], - model: "bge-m3", - usage: { prompt_tokens: 5, total_tokens: 5 }, - }), - }; - } - } - - return { default: MockOpenAI }; -}); - -vi.mock("@anthropic-ai/sdk", () => { - class MockAnthropic { - apiKey: string; - messages: { create: ReturnType; stream: ReturnType }; - - constructor(opts: { apiKey: string }) { - this.apiKey = opts.apiKey; - this.messages = { - create: vi.fn().mockResolvedValue({ - id: "msg_mock", - model: "claude-sonnet-4", - content: [{ type: "text", text: "Mock Anthropic response" }], - stop_reason: "end_turn", - usage: { input_tokens: 10, output_tokens: 5 }, - }), - stream: vi.fn(), - }; - } - } - - return { default: MockAnthropic }; -}); - -vi.mock("pg", () => { - const mockQuery = vi.fn(); - const mockConnect = vi.fn(); - const mockRelease = vi.fn(); - const mockClient = { query: mockQuery, release: mockRelease }; - - mockConnect.mockResolvedValue(mockClient); - - class MockPool { - query = mockQuery; - connect = mockConnect; - end = vi.fn(); - } - - return { - default: { Pool: MockPool }, - Pool: MockPool, - }; -}); - -vi.mock("@modelcontextprotocol/sdk/client/index.js", () => { - class MockClient { - connect = vi.fn(); - close = vi.fn(); - listTools = vi.fn().mockResolvedValue({ tools: [] }); - callTool = vi.fn().mockResolvedValue({ content: [{ type: "text", text: "mock" }] }); - } - - return { Client: MockClient }; -}); - -vi.mock("@modelcontextprotocol/sdk/client/stdio.js", () => { - return { StdioClientTransport: vi.fn() }; -}); - -vi.mock("@modelcontextprotocol/sdk/client/sse.js", () => { - return { SSEClientTransport: vi.fn() }; -}); - -vi.mock("@modelcontextprotocol/sdk/server/index.js", () => { - class MockServer { - setRequestHandler = vi.fn(); - connect = vi.fn(); - } - - return { Server: MockServer }; -}); - -vi.mock("@modelcontextprotocol/sdk/server/sse.js", () => { - const mockHandlePostMessage = vi.fn(); - class MockSSEServerTransport { - sessionId = "test-session-id"; - handlePostMessage = mockHandlePostMessage; - } - - return { SSEServerTransport: MockSSEServerTransport }; -}); - -vi.mock("@modelcontextprotocol/sdk/types.js", () => { - return { - ListToolsRequestSchema: {}, - CallToolRequestSchema: {}, - }; -}); - -// ── Mock node:fs for ToolRegistry / config loading tests ─────────────── -vi.mock("node:fs", () => { - const mockExistsSync = vi.fn(() => false); - const mockReadFileSync = vi.fn(() => ""); - - return { - default: { - existsSync: mockExistsSync, - readFileSync: mockReadFileSync, - }, - existsSync: mockExistsSync, - readFileSync: mockReadFileSync, - }; -}); - -// ── Global env vars ───────────────────────────────────────────────────── -process.env.OPENAI_API_KEY = "test-openai-key"; -process.env.OPENAI_BASE_URL = "https://api.openai.com/v1"; -process.env.OPENAI_DEFAULT_MODEL = "gpt-4o"; -process.env.GATEWAY_PORT = "0"; -process.env.GATEWAY_HOST = "127.0.0.1"; \ No newline at end of file diff --git a/rag-gateway/tests/unit/config.test.ts b/rag-gateway/tests/unit/config.test.ts deleted file mode 100644 index 7f96c78..0000000 --- a/rag-gateway/tests/unit/config.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { loadConfig, applyGatewayConfigEnv, gatewayEnvFromConfig } from "../../src/config.js"; - -describe("loadConfig", () => { - it("should read GATEWAY_PORT and GATEWAY_HOST from env", () => { - process.env.GATEWAY_PORT = "4000"; - process.env.GATEWAY_HOST = "0.0.0.0"; - const config = loadConfig(); - expect(config.port).toBe(4000); - expect(config.host).toBe("0.0.0.0"); - }); - - it("should use default base URL for embeddings", () => { - const config = loadConfig(); - expect(config.embeddingBaseUrl).toBe("https://api.openai.com/v1"); - }); - - it("should use default embedding model", () => { - const config = loadConfig(); - expect(config.embeddingModel).toBe("bge-m3"); - }); - - it("should parse GRAPH_RAG_ENABLED as boolean", () => { - process.env.GRAPH_RAG_ENABLED = "true"; - expect(loadConfig().graphRagEnabled).toBe(true); - process.env.GRAPH_RAG_ENABLED = "false"; - expect(loadConfig().graphRagEnabled).toBe(false); - process.env.GRAPH_RAG_ENABLED = ""; - expect(loadConfig().graphRagEnabled).toBe(false); - }); - - it("should return default value for optional vars", () => { - // dotenv may set these from .env, so verify they exist at least - const config = loadConfig(); - expect(typeof config.qdrantUrl).toBe("string"); - expect(typeof config.databaseUrl).toBe("string"); - }); -}); - -describe("applyGatewayConfigEnv", () => { - it("should set env vars from parsed JSON object", () => { - const env: Record = {}; - applyGatewayConfigEnv( - { EMBEDDING_API_KEY: "sk-abc", EXTRACTION_MODEL: "gpt-4o" }, - env, - ); - expect(env.EMBEDDING_API_KEY).toBe("sk-abc"); - expect(env.EXTRACTION_MODEL).toBe("gpt-4o"); - }); - - it("should not override existing env vars", () => { - const env = { EMBEDDING_API_KEY: "sk-existing" }; - applyGatewayConfigEnv({ EMBEDDING_API_KEY: "sk-abc" }, env); - expect(env.EMBEDDING_API_KEY).toBe("sk-existing"); - }); - - it("should skip null and non-string values", () => { - const env: Record = {}; - applyGatewayConfigEnv( - { SKIP_NULL: null, SKIP_NUMBER: 42, KEEP_STRING: "ok" }, - env, - ); - expect(env.SKIP_NULL).toBeUndefined(); - expect(env.SKIP_NUMBER).toBeUndefined(); - expect(env.KEEP_STRING).toBe("ok"); - }); -}); - -describe("gatewayEnvFromConfig", () => { - it("should get rag-gateway env from platforms config", () => { - const values = gatewayEnvFromConfig({ - env: {}, - platforms: { - "rag-gateway": { - env: { - EMBEDDING_BASE_URL: "https://rag-gateway.example", - EMBEDDING_API_KEY: "sk-rag-gateway", - }, - }, - }, - }); - - expect(values.EMBEDDING_BASE_URL).toBe("https://rag-gateway.example"); - expect(values.EMBEDDING_API_KEY).toBe("sk-rag-gateway"); - }); - - it("should keep legacy gateway env as a fallback", () => { - const values = gatewayEnvFromConfig({ - platforms: { - gateway: { - env: { - EMBEDDING_API_KEY: "sk-legacy", - }, - }, - }, - }); - - expect(values.EMBEDDING_API_KEY).toBe("sk-legacy"); - }); - - it("should let rag-gateway env override legacy gateway env", () => { - const values = gatewayEnvFromConfig({ - platforms: { - gateway: { - env: { - EMBEDDING_BASE_URL: "https://legacy.example", - }, - }, - "rag-gateway": { - env: { - EMBEDDING_BASE_URL: "https://rag-gateway.example", - }, - }, - }, - }); - - expect(values.EMBEDDING_BASE_URL).toBe("https://rag-gateway.example"); - }); -}); diff --git a/rag-gateway/tests/unit/db/graph.test.ts b/rag-gateway/tests/unit/db/graph.test.ts deleted file mode 100644 index 101726f..0000000 --- a/rag-gateway/tests/unit/db/graph.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -/** - * Unit tests for db/graph.ts — entity upsert with RETURNING and project_id update. - * - * These verify the two P0/P2 fixes: - * 1. upsertEntities returns a Map of name → persisted ID (the real DB id after - * ON CONFLICT, not the in-memory-generated UUID that may have been discarded). - * 2. The ON CONFLICT UPDATE SET clause includes project_id so cross-project - * entity search works correctly. - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// ── Mock db/index.ts so getPool() returns a controllable pool ──────── -// graph.ts calls getPool() which checks a module-level pool variable. -// We mock the module to return a pool whose .connect and .query we control. - -const mockQuery = vi.fn(); -const mockConnect = vi.fn(); -const mockRelease = vi.fn(); -const mockClient = { query: mockQuery, release: mockRelease }; - -vi.mock("../../../src/db/index.js", () => ({ - getPool: vi.fn(() => ({ - query: mockQuery, - connect: mockConnect, - end: vi.fn(), - })), -})); - -// Static import — vitest hoists vi.mock above this, so graph.ts sees the mocked getPool -import * as graphDb from "../../../src/db/graph.js"; -import type { StoreEntity } from "../../../src/db/graph.js"; - -beforeEach(() => { - vi.clearAllMocks(); - mockConnect.mockResolvedValue(mockClient); - mockRelease.mockResolvedValue(undefined); -}); - -// ── Test helpers ────────────────────────────────────────────────────── - -const baseEntity: StoreEntity = { - id: "550e8400-e29b-41d4-a716-446655440000", - tenantId: "tenant-1", - projectId: "project-1", - type: "technology", - name: "PostgreSQL", - properties: { lang: "SQL" }, -}; - -// ── upsertEntities batch ────────────────────────────────────────────── - -describe("upsertEntities [RETURNING fix]", () => { - it("returns a Map of name → persisted id for new entities", async () => { - // BEGIN → INSERT … ON CONFLICT … RETURNING → COMMIT - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: baseEntity.id, name: baseEntity.name }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - const result = await graphDb.upsertEntities([baseEntity]); - expect(result).toBeInstanceOf(Map); - expect(result.size).toBe(1); - expect(result.get("PostgreSQL")).toBe("550e8400-e29b-41d4-a716-446655440000"); - }); - - it("returns the EXISTING (persisted) id when ON CONFLICT triggers UPDATE", async () => { - // rows[0].id differs from what we passed in — ON CONFLICT preserved the old id - const existingId = "a1a1a1a1-b2b2-c3c3-d4d4-e5e5e5e5e5e5"; - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: existingId, name: "PostgreSQL" }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - const result = await graphDb.upsertEntities([baseEntity]); - expect(result.get("PostgreSQL")).toBe(existingId); - expect(result.get("PostgreSQL")).not.toBe(baseEntity.id); - }); - - it("returns persisted id for each entity in a batch (some new, some existing)", async () => { - const entities: StoreEntity[] = [ - { ...baseEntity, name: "Redis", id: "id-redis-new" }, - { ...baseEntity, name: "PostgreSQL", id: "id-pg-new" }, - { ...baseEntity, name: "Docker", id: "id-docker-new" }, - ]; - - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: "id-redis-new", name: "Redis" }] }) - .mockResolvedValueOnce({ rows: [{ id: "existing-pg-uuid", name: "PostgreSQL" }] }) - .mockResolvedValueOnce({ rows: [{ id: "id-docker-new", name: "Docker" }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - const result = await graphDb.upsertEntities(entities); - expect(result.size).toBe(3); - expect(result.get("Redis")).toBe("id-redis-new"); - expect(result.get("PostgreSQL")).toBe("existing-pg-uuid"); - expect(result.get("Docker")).toBe("id-docker-new"); - }); - - it("returns an empty Map for an empty array (no SQL run)", async () => { - const result = await graphDb.upsertEntities([]); - expect(result).toBeInstanceOf(Map); - expect(result.size).toBe(0); - expect(mockQuery).not.toHaveBeenCalled(); - expect(mockConnect).not.toHaveBeenCalled(); - }); - - it("handles projectId = undefined by passing null to SQL", async () => { - const entityNoProject: StoreEntity = { ...baseEntity, projectId: undefined }; - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: baseEntity.id, name: baseEntity.name }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - const result = await graphDb.upsertEntities([entityNoProject]); - expect(result.get(entityNoProject.name)).toBe(baseEntity.id); - }); - - it("rolls back on error and rethrows", async () => { - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockRejectedValueOnce(new Error("FK violation")); // INSERT fails - - await expect(graphDb.upsertEntities([baseEntity])).rejects.toThrow("FK violation"); - expect(mockRelease).toHaveBeenCalled(); - }); - - it("includes RETURNING id, name in the SQL", async () => { - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: baseEntity.id, name: baseEntity.name }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - await graphDb.upsertEntities([baseEntity]); - - // The INSERT is the second call (after BEGIN) - const insertCall = mockQuery.mock.calls[1]; - const sql = insertCall[0] as string; - expect(sql).toMatch(/RETURNING\s+id\s*,\s*name/i); - }); -}); - -// ── upsertEntity single ─────────────────────────────────────────────── - -describe("upsertEntity [single entity, RETURNING fix]", () => { - it("returns the persisted id string (not void)", async () => { - mockQuery.mockResolvedValue({ rows: [{ id: baseEntity.id }] }); - - const id = await graphDb.upsertEntity(baseEntity); - expect(id).toBe(baseEntity.id); - }); - - it("returns the existing id on conflict", async () => { - const existingId = "99999999-8888-7777-6666-555555555555"; - mockQuery.mockResolvedValue({ rows: [{ id: existingId }] }); - - const id = await graphDb.upsertEntity(baseEntity); - expect(id).toBe(existingId); - expect(id).not.toBe(baseEntity.id); - }); - - it("includes RETURNING id in the SQL", async () => { - mockQuery.mockResolvedValue({ rows: [{ id: baseEntity.id }] }); - await graphDb.upsertEntity(baseEntity); - - const sql = mockQuery.mock.calls[0][0] as string; - expect(sql).toMatch(/RETURNING\s+id\b/i); - }); -}); - -// ── project_id in UPDATE SET (both functions) ───────────────────────── - -describe("project_id is updated on ON CONFLICT", () => { - it("upsertEntity SQL includes project_id = COALESCE(...)", async () => { - mockQuery.mockResolvedValue({ rows: [{ id: baseEntity.id }] }); - await graphDb.upsertEntity(baseEntity); - - const sql = mockQuery.mock.calls[0][0] as string; - expect(sql).toMatch(/project_id\s*=/i); - }); - - it("upsertEntities SQL includes project_id = COALESCE(...)", async () => { - mockQuery - .mockResolvedValueOnce(undefined) // BEGIN - .mockResolvedValueOnce({ rows: [{ id: baseEntity.id, name: baseEntity.name }] }) - .mockResolvedValueOnce(undefined); // COMMIT - - await graphDb.upsertEntities([baseEntity]); - const sql = mockQuery.mock.calls[1][0] as string; - expect(sql).toMatch(/project_id\s*=/i); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/entity/extractor.test.ts b/rag-gateway/tests/unit/entity/extractor.test.ts deleted file mode 100644 index b36f2b8..0000000 --- a/rag-gateway/tests/unit/entity/extractor.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { EntityExtractor } from "../../../src/entity/extractor.js"; -import type { GatewayConfig } from "../../../src/config.js"; - -const mockConfig = { openaiApiKey: "test-key", openaiBaseUrl: "https://api.openai.com/v1", openaiDefaultModel: "gpt-4o" } as unknown as GatewayConfig; - -describe("EntityExtractor", () => { - let extractor: EntityExtractor; - - beforeEach(() => { - extractor = new EntityExtractor(mockConfig); - }); - - it("should return null for text < 20 chars", async () => { - expect(await extractor.extract("short")).toBeNull(); - }); - - it("should send correct prompt and parse JSON response", async () => { - const mockCreate = vi.fn().mockResolvedValue({ - choices: [{ - message: { - content: JSON.stringify({ - entities: [{ type: "project", name: "MyApp", properties: {} }], - relationships: [{ from: "MyApp", to: "DB", relation: "uses", properties: {} }], - }), - }, - }], - }); - (extractor as any).client = { chat: { completions: { create: mockCreate } } }; - - const result = await extractor.extract("MyApp uses PostgreSQL as its database."); - expect(mockCreate).toHaveBeenCalledWith( - expect.objectContaining({ - response_format: { type: "json_object" }, - temperature: 0.1, - }), - ); - expect(result).not.toBeNull(); - expect(result!.entities).toHaveLength(1); - expect(result!.relationships).toHaveLength(1); - }); - - it("should return null on invalid JSON response", async () => { - const mockCreate = vi.fn().mockResolvedValue({ - choices: [{ message: { content: "not json" } }], - }); - (extractor as any).client = { chat: { completions: { create: mockCreate } } }; - expect(await extractor.extract("Some valid text that is long enough for entity extraction.")).toBeNull(); - }); - - it("should return null when entities/relationships are missing", async () => { - const mockCreate = vi.fn().mockResolvedValue({ - choices: [{ message: { content: '{"foo":"bar"}' } }], - }); - (extractor as any).client = { chat: { completions: { create: mockCreate } } }; - expect(await extractor.extract("Some valid text for the purpose of testing entity extraction behavior.")).toBeNull(); - }); - - it("should return null on API error (catch and log)", async () => { - const mockCreate = vi.fn().mockRejectedValue(new Error("API Error")); - (extractor as any).client = { chat: { completions: { create: mockCreate } } }; - expect(await extractor.extract("Some valid text that is long enough for entity extraction purposes.")).toBeNull(); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/entity/store.test.ts b/rag-gateway/tests/unit/entity/store.test.ts deleted file mode 100644 index bf66f08..0000000 --- a/rag-gateway/tests/unit/entity/store.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Integration tests for EntityStore + db/graph.ts — verifies that extractAndStore - * correctly uses persisted IDs from upsertEntities when building edges. - * - * This is the critical P0 fix: previously, extractAndStore used in-memory UUIDs - * for edges, causing FK violations when ON CONFLICT discarded those UUIDs. - * Now it merges the DB-returned ids into the name→id map before constructing edges. - */ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// vi.mock factories are hoisted — use vi.hoisted() for shared references -const { mockUpsertEntities, mockInsertEdges, mockSearchEntities, mockGetSubgraph } = vi.hoisted(() => ({ - mockUpsertEntities: vi.fn(), - mockInsertEdges: vi.fn(), - mockSearchEntities: vi.fn(), - mockGetSubgraph: vi.fn(), -})); - -vi.mock("../../../src/db/graph.js", () => ({ - upsertEntities: mockUpsertEntities, - insertEdges: mockInsertEdges, - searchEntities: mockSearchEntities, - getSubgraph: mockGetSubgraph, -})); - -const { mockExtract } = vi.hoisted(() => ({ - mockExtract: vi.fn(), -})); - -vi.mock("../../../src/entity/extractor.js", () => ({ - EntityExtractor: class { - extract = mockExtract; - }, -})); - -import { EntityStore } from "../../../src/entity/store.js"; -import type { GatewayConfig } from "../../../src/config.js"; - -const mockConfig = { - openaiApiKey: "test-key", - openaiBaseUrl: "https://api.openai.com/v1", - openaiDefaultModel: "gpt-4o", - graphRagEnabled: true, -} as unknown as GatewayConfig; - -describe("EntityStore.extractAndStore [persisted ID forwarding]", () => { - let store: EntityStore; - - beforeEach(() => { - vi.clearAllMocks(); - store = new EntityStore(mockConfig); - }); - - it("uses persisted IDs from upsertEntities (not in-memory UUIDs) for edge refs", async () => { - mockExtract.mockResolvedValue({ - entities: [ - { type: "project", name: "MyApp", properties: {} }, - { type: "technology", name: "PostgreSQL", properties: {} }, - ], - relationships: [ - { from: "MyApp", to: "PostgreSQL", relation: "uses", properties: {} }, - ], - }); - - // Simulate ON CONFLICT: upsertEntities returns DIFFERENT IDs than the - // in-memory ones — this is the scenario that used to cause FK violations. - const persistedMyAppId = "11111111-1111-1111-1111-111111111111"; - const persistedPgId = "22222222-2222-2222-2222-222222222222"; - mockUpsertEntities.mockResolvedValue( - new Map([ - ["MyApp", persistedMyAppId], - ["PostgreSQL", persistedPgId], - ]), - ); - - await store.extractAndStore("MyApp uses PostgreSQL", "tenant-1", "project-1"); - - // insertEdges must be called with the PERSISTED IDs (not the random UUIDs - // that extractAndStore generated in-memory before calling upsertEntities) - expect(mockInsertEdges).toHaveBeenCalledTimes(1); - const edgesArg = mockInsertEdges.mock.calls[0][0]; - expect(edgesArg).toHaveLength(1); - - const edge = edgesArg[0]; - expect(edge.fromEntityId).toBe(persistedMyAppId); - expect(edge.toEntityId).toBe(persistedPgId); - }); - - it("does not insert edges when a relationship references an entity not in the batch", async () => { - mockExtract.mockResolvedValue({ - entities: [ - { type: "project", name: "MyApp", properties: {} }, - ], - relationships: [ - { from: "MyApp", to: "MissingEntity", relation: "depends_on", properties: {} }, - ], - }); - - mockUpsertEntities.mockResolvedValue( - new Map([["MyApp", "persisted-myapp-id"]]), - ); - - await store.extractAndStore("MyApp depends on something missing", "tenant-1"); - expect(mockInsertEdges).not.toHaveBeenCalled(); - }); - - it("does not insert edges when no entities were extracted", async () => { - mockExtract.mockResolvedValue({ - entities: [], - relationships: [], - }); - - await store.extractAndStore("Nothing useful", "tenant-1"); - expect(mockUpsertEntities).not.toHaveBeenCalled(); - expect(mockInsertEdges).not.toHaveBeenCalled(); - }); - - it("skips duplicate entity names within the same batch", async () => { - mockExtract.mockResolvedValue({ - entities: [ - { type: "project", name: "MyApp", properties: {} }, - { type: "project", name: "MyApp", properties: { note: "duplicate" } }, - ], - relationships: [], - }); - - mockUpsertEntities.mockResolvedValue(new Map([["MyApp", "persisted-myapp-id"]])); - - await store.extractAndStore("MyApp mentioned twice", "tenant-1"); - expect(mockUpsertEntities).toHaveBeenCalledTimes(1); - const upsertArg = mockUpsertEntities.mock.calls[0][0]; - expect(upsertArg).toHaveLength(1); - }); -}); - -describe("EntityStore.searchGraph", () => { - let store: EntityStore; - - beforeEach(() => { - vi.clearAllMocks(); - store = new EntityStore(mockConfig); - }); - - it("returns empty array when searchEntities returns nothing", async () => { - mockSearchEntities.mockResolvedValue([]); - - const result = await store.searchGraph("query", "tenant-1"); - expect(result).toEqual([]); - expect(mockGetSubgraph).not.toHaveBeenCalled(); - }); - - it("searches with projectId filter when provided", async () => { - mockSearchEntities.mockResolvedValue([]); - - await store.searchGraph("query", "tenant-1", { projectId: "proj-1" }); - expect(mockSearchEntities).toHaveBeenCalledWith( - "query", - "tenant-1", - expect.objectContaining({ projectId: "proj-1", limit: 5 }), - ); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/mcp/client.test.ts b/rag-gateway/tests/unit/mcp/client.test.ts deleted file mode 100644 index 7ac89fa..0000000 --- a/rag-gateway/tests/unit/mcp/client.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { McpClientManager } from "../../../src/mcp/client.js"; - -describe("McpClientManager", () => { - let manager: McpClientManager; - - beforeEach(() => { - manager = new McpClientManager(); - }); - - it("should be empty on construction", () => { - expect(manager.count).toBe(0); - }); - - it("registerServers should add entries", () => { - const configs = new Map(); - configs.set("test-server", { command: "npx", args: ["-y", "test"] }); - manager.registerServers(configs); - expect(manager.count).toBe(1); - }); - - it("registerServers should skip duplicates", () => { - const configs = new Map(); - configs.set("test-server", { command: "npx" }); - manager.registerServers(configs); - manager.registerServers(configs); - expect(manager.count).toBe(1); - }); - - it("getState should return disconnected for unknown server", () => { - expect(manager.getState("unknown")).toBe("disconnected"); - }); - - it("getClient should return undefined before connection", () => { - const configs = new Map(); - configs.set("svr", { command: "npx" }); - manager.registerServers(configs); - expect(manager.getClient("svr")).toBeUndefined(); - }); - - it("closeAll should not throw when no servers", async () => { - await manager.closeAll(); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/mcp/config.test.ts b/rag-gateway/tests/unit/mcp/config.test.ts deleted file mode 100644 index b4f425c..0000000 --- a/rag-gateway/tests/unit/mcp/config.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { loadMcpServerConfigs } from "../../../src/mcp/config.js"; - -vi.mock("node:fs"); - -describe("loadMcpServerConfigs", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("should return empty Map for non-existent file", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(false); - const result = loadMcpServerConfigs("/no/file.json"); - expect(result.size).toBe(0); - }); - - it("should parse valid config file correctly", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue(JSON.stringify({ - mcpServers: { - filesystem: { command: "npx", args: ["-y", "fs-server"] }, - web: { url: "https://example.com/mcp", headers: { Authorization: "Bearer token" } }, - }, - })); - const result = loadMcpServerConfigs("/config.json"); - expect(result.size).toBe(2); - expect(result.get("filesystem")!.command).toBe("npx"); - expect(result.get("web")!.url).toBe("https://example.com/mcp"); - }); - - it("should skip entries missing command and url", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue(JSON.stringify({ - mcpServers: { - valid: { command: "npx" }, - invalid: {}, - }, - })); - const result = loadMcpServerConfigs("/config.json"); - expect(result.size).toBe(1); - expect(result.has("valid")).toBe(true); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/metrics.test.ts b/rag-gateway/tests/unit/metrics.test.ts deleted file mode 100644 index a5bfb91..0000000 --- a/rag-gateway/tests/unit/metrics.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { metricsCollector } from "../../src/metrics.js"; - -describe("MetricsCollector", () => { - beforeEach(() => { - // Reset internal state through snapshot-and-ignore (no reset API) - // We test discrete behaviors rather than cumulative totals - }); - - it("recordRequest should update counts", () => { - metricsCollector.recordRequest("gpt-4o", 100, "200"); - const snap = metricsCollector.snapshot(); - expect(snap.requestsTotal).toBeGreaterThanOrEqual(1); - expect(snap.requestsByModel["gpt-4o"]).toBeGreaterThanOrEqual(1); - expect(snap.requestsByStatus["200"]).toBeGreaterThanOrEqual(1); - }); - - it("recordToolCall and retrieval/entity methods should increment", () => { - metricsCollector.recordToolCall(3); - metricsCollector.recordRetrievalHits(5); - metricsCollector.recordEntityExtraction(2); - const snap = metricsCollector.snapshot(); - expect(snap.toolCallsTotal).toBeGreaterThanOrEqual(3); - expect(snap.retrievalHitsTotal).toBeGreaterThanOrEqual(5); - expect(snap.entitiesExtractedTotal).toBeGreaterThanOrEqual(2); - }); - - it("snapshot should return MetricsSnapshot with histogram", () => { - const snap = metricsCollector.snapshot(); - expect(snap).toHaveProperty("uptimeMs"); - expect(snap).toHaveProperty("latencyHistogramMs"); - expect(snap).toHaveProperty("recentDegradations"); - expect(snap.latencyHistogramMs["0-100"]).toBeTypeOf("number"); - }); - - it("recordDegradation should add to recentDegradations", () => { - metricsCollector.recordDegradation("test-component", "test reason"); - const snap = metricsCollector.snapshot(); - const found = snap.recentDegradations.find((d) => d.component === "test-component"); - expect(found).toBeDefined(); - expect(found!.reason).toBe("test reason"); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/tool/executor.test.ts b/rag-gateway/tests/unit/tool/executor.test.ts deleted file mode 100644 index 37a6468..0000000 --- a/rag-gateway/tests/unit/tool/executor.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; -import { ToolExecutorEngine } from "../../../src/tool/executor.js"; -import type { ToolSpec } from "../../../src/tool/types.js"; - -describe("ToolExecutorEngine", () => { - let engine: ToolExecutorEngine; - let fetchMock: ReturnType; - - beforeEach(() => { - fetchMock = vi.fn(); - vi.stubGlobal("fetch", fetchMock); - engine = new ToolExecutorEngine( - ["api.github.com", "httpbin.org", "jsonplaceholder.typicode.com"], - ); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - const httpTool: ToolSpec = { - name: "lookup_status", - description: "Look up HTTP status code", - input_schema: { type: "object", properties: { code: { type: "string" } }, required: ["code"] }, - executor: { type: "http_request", method: "GET", url: "https://httpbin.org/status/{{args.code}}", timeoutMs: 5000 }, - }; - - const templateTool: ToolSpec = { - name: "get_time", - description: "Get current time", - input_schema: { type: "object", properties: {} }, - executor: { type: "static_template", template: "Current time: {{env.NOW}}" }, - }; - - // ── http_request tests ────────────────────────────────────────────── - - it("should execute http_request with GET method", async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - text: () => Promise.resolve("OK response"), - }); - const result = await engine.execute(httpTool, "call-1", { code: "200" }); - expect(result.success).toBe(true); - expect(fetchMock).toHaveBeenCalledWith( - "https://httpbin.org/status/200", - expect.objectContaining({ method: "GET" }), - ); - }); - - it("should block non-HTTPS URLs (except httpbin and localhost)", async () => { - const badTool: ToolSpec = { - ...httpTool, - executor: { type: "http_request", method: "GET", url: "http://evil.com/data" }, - }; - const result = await engine.execute(badTool, "call-2", {}); - expect(result.success).toBe(false); - expect(result.error).toContain("Non-HTTPS"); - }); - - it("should allow httpbin.org on HTTP", async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - text: () => Promise.resolve("OK"), - }); - const httpbinTool: ToolSpec = { - ...httpTool, - executor: { type: "http_request", method: "GET", url: "http://httpbin.org/get" }, - }; - const result = await engine.execute(httpbinTool, "call-3", {}); - expect(result.success).toBe(true); - }); - - it("should block non-allowlisted hosts", async () => { - const blockTool: ToolSpec = { - ...httpTool, - executor: { type: "http_request", method: "GET", url: "https://random-site.com/data" }, - }; - const result = await engine.execute(blockTool, "call-4", {}); - expect(result.success).toBe(false); - expect(result.error).toContain("allowlist"); - }); - - it("should enforce timeout via AbortController", async () => { - fetchMock.mockImplementation( - () => new Promise((_, reject) => { - setTimeout(() => reject(new Error("timeout")), 100); - }), - ); - const fastTool: ToolSpec = { - ...httpTool, - executor: { type: "http_request", method: "GET", url: "https://httpbin.org/status/200", timeoutMs: 10 }, - }; - const result = await engine.execute(fastTool, "call-5", { code: "200" }); - expect(result.success).toBe(false); - }); - - it("should handle POST with body and header template substitution", async () => { - fetchMock.mockResolvedValue({ - ok: true, status: 200, text: () => Promise.resolve("posted"), - }); - const postTool: ToolSpec = { - name: "post_data", - description: "POST data", - input_schema: { type: "object", properties: { value: { type: "string" } }, required: ["value"] }, - executor: { - type: "http_request", method: "POST", - url: "https://jsonplaceholder.typicode.com/posts", - headers: { "X-Custom": "test-header" }, - body: { key: "{{args.value}}" }, - }, - }; - const result = await engine.execute(postTool, "call-6", { value: "hello" }); - expect(result.success).toBe(true); - const callArgs = fetchMock.mock.calls[0]; - expect(callArgs[1].method).toBe("POST"); - expect(JSON.parse(callArgs[1].body)).toEqual({ key: "hello" }); - expect(callArgs[1].headers["X-Custom"]).toBe("test-header"); - }); - - // ── static_template tests ─────────────────────────────────────────── - - it("should execute static_template with {{args.xxx}} substitution", async () => { - const tool: ToolSpec = { - name: "greet", - description: "Greet someone", - input_schema: { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, - executor: { type: "static_template", template: "Hello, {{args.name}}!" }, - }; - const result = await engine.execute(tool, "call-t1", { name: "Alice" }); - expect(result.success).toBe(true); - expect(result.content).toBe("Hello, Alice!"); - }); - - it("should execute static_template with {{env.NOW}} substitution", async () => { - const tool: ToolSpec = { - name: "time", - description: "Get time", - input_schema: { type: "object", properties: {} }, - executor: { type: "static_template", template: "Time: {{env.NOW}}" }, - }; - const result = await engine.execute(tool, "call-t2", {}); - expect(result.success).toBe(true); - expect(result.content).toMatch(/Time: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:/); - }); - - it("should substitute environment variables via {{env.VAR}}", async () => { - process.env.TEST_VAR = "test_value"; - const tool: ToolSpec = { - name: "env_test", - description: "Test env vars", - input_schema: { type: "object", properties: {} }, - executor: { type: "static_template", template: "Value: {{env.TEST_VAR}}" }, - }; - const result = await engine.execute(tool, "call-t3", {}); - expect(result.content).toBe("Value: test_value"); - delete process.env.TEST_VAR; - }); - - // ── Unknown executor type ─────────────────────────────────────────── - - it("should return error for unknown executor type", async () => { - const badTool: ToolSpec = { - name: "bad", - description: "Bad tool", - input_schema: { type: "object", properties: {} }, - executor: { type: "unknown_executor" as any }, - }; - const result = await engine.execute(badTool, "call-u1", {}); - expect(result.success).toBe(false); - expect(result.error).toContain("Unknown executor type"); - }); - - // ── executeBatch ──────────────────────────────────────────────────── - - it("should execute multiple tools via executeBatch", async () => { - fetchMock.mockResolvedValue({ - ok: true, status: 200, text: () => Promise.resolve("OK"), - }); - const results = await engine.executeBatch( - [httpTool, templateTool], - [ - { id: "batch-1", name: "lookup_status", args: { code: "200" } }, - { id: "batch-2", name: "get_time", args: {} }, - ], - ); - expect(results).toHaveLength(2); - expect(results[0].success).toBe(true); - expect(results[1].success).toBe(true); - }); - - it("should return error for unknown tool in executeBatch", async () => { - const results = await engine.executeBatch( - [httpTool], - [{ id: "batch-x", name: "nonexistent_tool", args: {} }], - ); - expect(results).toHaveLength(1); - expect(results[0].success).toBe(false); - expect(results[0].error).toContain("not found"); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/tool/registry.test.ts b/rag-gateway/tests/unit/tool/registry.test.ts deleted file mode 100644 index ba3fc0c..0000000 --- a/rag-gateway/tests/unit/tool/registry.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { ToolRegistry } from "../../../src/tool/registry.js"; -import type { ToolSpec } from "../../../src/tool/types.js"; - -describe("ToolRegistry", () => { - let registry: ToolRegistry; - - beforeEach(() => { - registry = new ToolRegistry(); - }); - - it("should be empty on construction", () => { - expect(registry.count).toBe(0); - }); - - it("should load tools from a valid JSON array", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue( - JSON.stringify([ - { name: "t1", description: "T1", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "h" } }, - { name: "t2", description: "T2", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "w" } }, - ]), - ); - registry.loadFromFile("/fake/path.json"); - expect(registry.count).toBe(2); - }); - - it("should load tools from { tools: [...] } format", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue( - JSON.stringify({ tools: [{ name: "t1", description: "T1", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "h" } }] }), - ); - registry.loadFromFile("/fake/path.json"); - expect(registry.count).toBe(1); - }); - - it("should skip specs missing name", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue( - JSON.stringify([{ description: "No name", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" } }]), - ); - registry.loadFromFile("/fake/path.json"); - expect(registry.count).toBe(0); - }); - - it("should skip specs missing executor", async () => { - const fs = await import("node:fs"); - (fs.existsSync as ReturnType).mockReturnValue(true); - (fs.readFileSync as ReturnType).mockReturnValue( - JSON.stringify([{ name: "no_exec", description: "ME", input_schema: { type: "object", properties: {} } }]), - ); - registry.loadFromFile("/fake/path.json"); - expect(registry.count).toBe(0); - }); - - it("should register a tool programmatically", () => { - registry.register({ name: "test_tool", description: "Test tool", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "test" } }); - expect(registry.count).toBe(1); - }); - - it("getActiveTools should return only non-internal active tools", () => { - registry.register({ name: "public", description: "P", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "x" } }); - registry.register({ name: "internal", description: "I", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "y" }, internal: true }); - expect(registry.getActiveTools().map((t) => t.name)).toEqual(["public"]); - }); - - it("shouldInjectTool with blocklist", () => { - const spec: ToolSpec = { name: "b", description: "b", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" }, disableForModels: ["gpt-3.5-*"] }; - expect(registry.shouldInjectTool(spec, "gpt-3.5-turbo")).toBe(false); - expect(registry.shouldInjectTool(spec, "gpt-4o")).toBe(true); - }); - - it("shouldInjectTool with allowlist", () => { - const spec: ToolSpec = { name: "p", description: "p", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" }, compatibleModels: ["claude-*", "gpt-4-*"] }; - expect(registry.shouldInjectTool(spec, "claude-sonnet-4")).toBe(true); - expect(registry.shouldInjectTool(spec, "gpt-4-turbo")).toBe(true); - expect(registry.shouldInjectTool(spec, "gpt-3.5-turbo")).toBe(false); - }); - - it("shouldInjectTool no allowlist or blocklist → always true", () => { - const spec: ToolSpec = { name: "u", description: "u", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" } }; - expect(registry.shouldInjectTool(spec, "any-model")).toBe(true); - }); - - it("glob '*' matches all models", () => { - const spec: ToolSpec = { name: "c", description: "c", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" }, disableForModels: ["*"] }; - expect(registry.shouldInjectTool(spec, "anything")).toBe(false); - }); - - it("resolveToolChoicePolicy", () => { - registry.register({ name: "pt", description: "p", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "t" }, toolChoicePolicy: { "claude-sonnet-4-*": "any" } }); - expect(registry.resolveToolChoicePolicy("claude-sonnet-4-20250514")).toBe("any"); - expect(registry.resolveToolChoicePolicy("gpt-4o")).toBeUndefined(); - }); - - it("toOpenAITool shape", () => { - const r = registry.toOpenAITool({ name: "mt", description: "MT", input_schema: { type: "object", properties: { x: { type: "string" } }, required: ["x"] }, executor: { type: "static_template", template: "t" } }); - expect(r.type).toBe("function"); - expect(r.function.name).toBe("mt"); - expect(r.function.parameters).toEqual({ type: "object", properties: { x: { type: "string" } }, required: ["x"] }); - }); - - it("get by name", () => { - registry.register({ name: "find_me", description: "FM", input_schema: { type: "object", properties: {} }, executor: { type: "static_template", template: "f" } }); - expect(registry.get("find_me")).toBeDefined(); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/tool/simplify.test.ts b/rag-gateway/tests/unit/tool/simplify.test.ts deleted file mode 100644 index 863fc46..0000000 --- a/rag-gateway/tests/unit/tool/simplify.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getSimplifiedSchema } from "../../../src/tool/simplify.js"; -import type { ToolSpec } from "../../../src/tool/types.js"; - -const complexSchema: ToolSpec["input_schema"] = { - type: "object", - properties: { - name: { type: "string", description: "The name of the user to look up. Must be a valid username with no special characters." }, - age: { type: "number", description: "Age of the user in years." }, - address: { type: "object", properties: { street: { type: "string" }, city: { type: "string" } }, description: "Full address object." }, - tags: { type: "array", items: { type: "object", properties: { key: { type: "string" }, value: { type: "string" } } }, description: "List of tags." }, - status: { type: "string", description: "Current status.", enum: ["active", "inactive", "suspended"] }, - }, - required: ["name", "age", "address", "tags", "status", "extra"], -}; - -const complexTool: ToolSpec = { - name: "complex_tool", - description: "A tool with complex schema", - input_schema: complexSchema, - executor: { type: "static_template", template: "done" }, - schemaSimplifyFor: ["claude-3-haiku-*"], -}; - -describe("getSimplifiedSchema", () => { - it("should return original schema when schemaSimplifyFor is empty", () => { - const tool: ToolSpec = { - name: "simple", description: "Simple", input_schema: { type: "object", properties: { x: { type: "string" } } }, - executor: { type: "static_template", template: "done" }, - }; - const result = getSimplifiedSchema(tool, "any-model"); - expect(result).toBe(tool.input_schema); - }); - - it("should return original schema when model is not in schemaSimplifyFor", () => { - const result = getSimplifiedSchema(complexTool, "gpt-4o"); - expect(result).toBe(complexSchema); - }); - - it("should simplify when model matches schemaSimplifyFor", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - expect(result).not.toBe(complexSchema); - expect(result.type).toBe("object"); - }); - - it("should flatten nested object properties", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - const addr = result.properties["address"] as Record; - expect(addr.type).toBe("object"); - expect(Object.keys((addr.properties as Record) || {})).toEqual([]); - }); - - it("should change array with nested object items to string items", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - const tags = result.properties["tags"] as Record; - expect(tags.type).toBe("array"); - const items = tags.items as Record; - expect(items.type).toBe("string"); - }); - - it("should keep scalar types and enums", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - const status = result.properties["status"] as Record; - expect(status.type).toBe("string"); - expect((status as any).enum).toEqual(["active", "inactive", "suspended"]); - }); - - it("should clip required to first 5 fields", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - expect(result.required!.length).toBeLessThanOrEqual(5); - }); - - it("should shorten descriptions", () => { - const result = getSimplifiedSchema(complexTool, "claude-3-haiku-20240307"); - const name = result.properties["name"] as Record; - // Original is 85 chars; simplified shortens it to first line - expect((name.description as string).length).toBeLessThanOrEqual(85); - }); - - it("should match pattern '*'", () => { - const tool: ToolSpec = { - name: "catchall_simplify", description: "Catchall", - input_schema: complexSchema, - executor: { type: "static_template", template: "done" }, - schemaSimplifyFor: ["*"], - }; - const result = getSimplifiedSchema(tool, "any-model"); - expect(result).not.toBe(complexSchema); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/vector/embedding.test.ts b/rag-gateway/tests/unit/vector/embedding.test.ts deleted file mode 100644 index a8178e3..0000000 --- a/rag-gateway/tests/unit/vector/embedding.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { EmbeddingService } from "../../../src/vector/embedding.js"; -import type { GatewayConfig } from "../../../src/config.js"; - -const mockConfig = { embeddingApiKey: "test-key", embeddingBaseUrl: "https://api.openai.com/v1", embeddingModel: "text-embedding-3-small" } as GatewayConfig; - -describe("EmbeddingService", () => { - beforeEach(() => { - vi.restoreAllMocks(); - }); - - it("should store config on construction", () => { - const svc = new EmbeddingService(mockConfig); - expect((svc as any).apiKey).toBe("test-key"); - expect((svc as any).baseUrl).toBe("https://api.openai.com/v1"); - expect((svc as any).model).toBe("text-embedding-3-small"); - }); - - it("should call fetch and return embedding for single embed", async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ - data: [ - { embedding: [0.1, 0.2, 0.3], index: 0 }, - ], - }), - }); - vi.stubGlobal("fetch", mockFetch); - - const svc = new EmbeddingService(mockConfig); - const result = await svc.embed("hello world"); - - expect(mockFetch).toHaveBeenCalledTimes(1); - const callUrl = mockFetch.mock.calls[0][0]; - const callBody = JSON.parse(mockFetch.mock.calls[0][1].body); - expect(callUrl).toBe("https://api.openai.com/v1/embeddings"); - expect(callBody.model).toBe("text-embedding-3-small"); - expect(callBody.input).toBe("hello world"); - expect(result).toEqual([0.1, 0.2, 0.3]); - }); - - it("should return embeddings for batch embed", async () => { - const mockFetch = vi.fn().mockResolvedValue({ - ok: true, - json: () => Promise.resolve({ - data: [ - { embedding: [0.1, 0.2], index: 0 }, - { embedding: [0.3, 0.4], index: 1 }, - ], - }), - }); - vi.stubGlobal("fetch", mockFetch); - - const svc = new EmbeddingService(mockConfig); - const result = await svc.embedBatch(["a", "b"]); - - expect(mockFetch).toHaveBeenCalledTimes(1); - const callBody = JSON.parse(mockFetch.mock.calls[0][1].body); - expect(callBody.input).toEqual(["a", "b"]); - expect(result).toHaveLength(2); - expect(result[0]).toEqual([0.1, 0.2]); - expect(result[1]).toEqual([0.3, 0.4]); - }); - - it("should return empty for empty batch", async () => { - const svc = new EmbeddingService(mockConfig); - const result = await svc.embedBatch([]); - expect(result).toEqual([]); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/vector/qdrant.test.ts b/rag-gateway/tests/unit/vector/qdrant.test.ts deleted file mode 100644 index eeadaf8..0000000 --- a/rag-gateway/tests/unit/vector/qdrant.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, it, expect, vi, afterEach } from "vitest"; -import { QdrantStore } from "../../../src/vector/qdrant.js"; - -describe("QdrantStore", () => { - let store: QdrantStore; - let fetchMock: ReturnType; - - beforeEach(() => { - fetchMock = vi.fn(); - vi.stubGlobal("fetch", fetchMock); - store = new QdrantStore("http://localhost:6333"); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - function mockJsonResponse(result: any, status = 200) { - fetchMock.mockResolvedValue({ - ok: status < 400, - status, - text: () => Promise.resolve(status >= 400 ? "error" : ""), - json: () => Promise.resolve({ result, status: "ok" }), - }); - } - - it("should strip trailing slash from URL", () => { - const s = new QdrantStore("http://localhost:6333/"); - expect((s as any).baseUrl).toBe("http://localhost:6333"); - }); - - it("ensureCollection should create collection if missing", async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, status: 200, - text: () => Promise.resolve(""), - json: () => Promise.resolve({ result: { collections: [] }, status: "ok" }), - }); - fetchMock.mockResolvedValueOnce({ - ok: true, status: 200, - text: () => Promise.resolve(""), - json: () => Promise.resolve({ result: {}, status: "ok" }), - }); - - await store.ensureCollection(); - // First call: GET /collections - expect(fetchMock).toHaveBeenCalledWith( - "http://localhost:6333/collections", - expect.objectContaining({ method: "GET" }), - ); - // Second call: PUT to create collection - const secondCall = fetchMock.mock.calls[1]; - expect(secondCall[0]).toBe("http://localhost:6333/collections/memory_chunks"); - expect(JSON.parse(secondCall[1].body).vectors.size).toBe(1024); - }); - - it("ensureCollection should skip creation if exists", async () => { - fetchMock.mockResolvedValueOnce({ - ok: true, status: 200, - text: () => Promise.resolve(""), - json: () => Promise.resolve({ result: { collections: [{ name: "memory_chunks" }] }, status: "ok" }), - }); - await store.ensureCollection(); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it("upsert should send correct payload", async () => { - mockJsonResponse({}); - await store.upsert("id-1", [0.1, 0.2], { text: "hello", tenantId: "default" }); - expect(fetchMock).toHaveBeenCalledWith( - "http://localhost:6333/collections/memory_chunks/points", - expect.objectContaining({ method: "PUT" }), - ); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.points[0].id).toBe("id-1"); - expect(body.points[0].vector).toEqual([0.1, 0.2]); - }); - - it("search should POST with vector and filter", async () => { - mockJsonResponse([ - { id: "p1", score: 0.9, payload: { text: "test", tenantId: "default", kind: "user_message", createdAt: "now" } }, - ]); - const result = await store.search([0.1, 0.2], { limit: 5, tenantId: "default" }); - expect(fetchMock).toHaveBeenCalledWith( - "http://localhost:6333/collections/memory_chunks/points/search", - expect.objectContaining({ method: "POST" }), - ); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.vector).toEqual([0.1, 0.2]); - expect(body.filter.must).toBeDefined(); - expect(result).toHaveLength(1); - expect(result[0].score).toBe(0.9); - }); - - it("search without tenantId should omit filter", async () => { - mockJsonResponse([]); - await store.search([0.1], { limit: 3 }); - const body = JSON.parse(fetchMock.mock.calls[0][1].body); - expect(body.filter).toBeUndefined(); - }); - - it("deleteCollection should send DELETE", async () => { - mockJsonResponse({}); - await store.deleteCollection(); - expect(fetchMock).toHaveBeenCalledWith( - "http://localhost:6333/collections/memory_chunks", - expect.objectContaining({ method: "DELETE" }), - ); - }); - - it("should throw on non-ok response", async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 500, - text: () => Promise.resolve("Internal error"), - }); - await expect(store.search([0.1])).rejects.toThrow("Qdrant POST"); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tests/unit/vector/store.test.ts b/rag-gateway/tests/unit/vector/store.test.ts deleted file mode 100644 index 47ef4f5..0000000 --- a/rag-gateway/tests/unit/vector/store.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { VectorStore } from "../../../src/vector/store.js"; -import type { ChunkedMessage } from "../../../src/vector/store.js"; - -describe("VectorStore", () => { - let store: VectorStore; - let mockEmbed: any; - let mockQdrant: any; - - beforeEach(() => { - mockEmbed = { embed: vi.fn(), embedBatch: vi.fn() }; - mockQdrant = { upsert: vi.fn(), upsertBatch: vi.fn(), search: vi.fn() }; - store = new VectorStore(mockEmbed as any, mockQdrant as any); - }); - - it("chunkMessage should split on double-newlines", () => { - const msg: ChunkedMessage = { - id: "1", text: "Para one.\n\nPara two.\n\nPara three.", - kind: "user_message", tenantId: "default", sourceMessageId: "s1", conversationId: "c1", - }; - const chunks = store.chunkMessage(msg); - expect(chunks.length).toBe(3); - expect(chunks[0].text).toBe("Para one."); - expect(chunks[1].text).toBe("Para two."); - }); - - it("chunkMessage should split long paragraphs by char boundary", () => { - const msg: ChunkedMessage = { - id: "2", text: "A".repeat(1200), - kind: "user_message", tenantId: "default", sourceMessageId: "s2", conversationId: "c2", - }; - const chunks = store.chunkMessage(msg, 500); - expect(chunks.length).toBe(3); // 500 + 500 + 200 - }); - - it("chunkMessage empty text returns empty", () => { - const msg: ChunkedMessage = { - id: "3", text: " ", - kind: "user_message", tenantId: "default", sourceMessageId: "s3", conversationId: "c3", - }; - expect(store.chunkMessage(msg)).toHaveLength(0); - }); - - it("chunkMessage short text returns single segment", () => { - const msg: ChunkedMessage = { - id: "4", text: "Short text", - kind: "user_message", tenantId: "default", sourceMessageId: "s4", conversationId: "c4", - }; - expect(store.chunkMessage(msg)).toHaveLength(1); - }); - - it("indexMessage should chunk, embed, and upsert", async () => { - mockEmbed.embedBatch.mockResolvedValue([[0.1], [0.2]]); - mockQdrant.upsertBatch.mockResolvedValue(undefined); - - const msg: ChunkedMessage = { - id: "5", text: "Hello.\n\nWorld.", kind: "user_message", - tenantId: "default", sourceMessageId: "s5", conversationId: "c5", - }; - await store.indexMessage(msg); - expect(mockEmbed.embedBatch).toHaveBeenCalledWith(["Hello.", "World."]); - expect(mockQdrant.upsertBatch).toHaveBeenCalled(); - const upsertArg = mockQdrant.upsertBatch.mock.calls[0][0]; - expect(upsertArg).toHaveLength(2); - }); - - it("indexMessages should batch all segments across messages", async () => { - mockEmbed.embedBatch.mockResolvedValue([[0.1]]); - mockQdrant.upsertBatch.mockResolvedValue(undefined); - - await store.indexMessages([ - { id: "a", text: "Message A", kind: "user_message", tenantId: "default", sourceMessageId: "sa", conversationId: "ca" }, - ]); - expect(mockEmbed.embedBatch).toHaveBeenCalled(); - expect(mockQdrant.upsertBatch).toHaveBeenCalled(); - }); - - it("search should embed query then call qdrant.search", async () => { - mockEmbed.embed.mockResolvedValue([0.1, 0.2]); - mockQdrant.search.mockResolvedValue([{ id: "p1", score: 0.9, payload: { text: "test", tenantId: "default", kind: "user_message", createdAt: "now" } }]); - - const results = await store.search("test query", { limit: 3, tenantId: "default" }); - expect(mockEmbed.embed).toHaveBeenCalledWith("test query"); - expect(mockQdrant.search).toHaveBeenCalledWith([0.1, 0.2], { limit: 3, tenantId: "default" }); - expect(results).toHaveLength(1); - }); -}); \ No newline at end of file diff --git a/rag-gateway/tools.json b/rag-gateway/tools.json deleted file mode 100644 index 4b4891e..0000000 --- a/rag-gateway/tools.json +++ /dev/null @@ -1,94 +0,0 @@ -[ - { - "name": "get_current_time", - "description": "Get the current date and time in ISO 8601 format.", - "input_schema": { - "type": "object", - "properties": {}, - "required": [] - }, - "executor": { - "type": "static_template", - "template": "Current time: {{env.NOW}}", - "contentType": "text/plain" - } - }, - { - "name": "lookup_http_status", - "description": "Look up the meaning of an HTTP status code from httpbin.org.", - "input_schema": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "HTTP status code (e.g., '418', '200', '404')" - } - }, - "required": [ - "code" - ] - }, - "executor": { - "type": "http_request", - "method": "GET", - "url": "https://httpbin.org/status/{{args.code}}", - "timeoutMs": 5000 - } - }, - { - "name": "get_definition", - "description": "Get a brief definition of a technical term from an open API.", - "input_schema": { - "type": "object", - "properties": { - "term": { - "type": "string", - "description": "Technical term to define" - } - }, - "required": [ - "term" - ] - }, - "executor": { - "type": "http_request", - "method": "GET", - "url": "https://api.urbandictionary.com/v0/define", - "query": { - "term": "{{args.term}}" - }, - "timeoutMs": 5000 - } - }, - { - "name": "search_memory", - "description": "Search semantic memory (Qdrant vector store) and entity graph (GraphRAG) for relevant context from past conversations.", - "input_schema": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The search query to find relevant memories and entities" - }, - "limit": { - "type": "number", - "description": "Maximum number of results to return (default: 5)" - }, - "tenant_id": { - "type": "string", - "description": "Tenant ID for multi-tenant isolation (default: 'default')" - }, - "project_id": { - "type": "string", - "description": "Optional project ID for project-scoped search" - } - }, - "required": ["query"] - }, - "executor": { - "type": "memory_search", - "defaultLimit": 5, - "scoreThreshold": 0.35 - } - } -] \ No newline at end of file diff --git a/rag-gateway/tsconfig.json b/rag-gateway/tsconfig.json deleted file mode 100644 index 226ca0c..0000000 --- a/rag-gateway/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2024", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true - }, - "include": [ - "src/**/*" - ] -} \ No newline at end of file diff --git a/rag-gateway/vitest.config.ts b/rag-gateway/vitest.config.ts deleted file mode 100644 index bbf744e..0000000 --- a/rag-gateway/vitest.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/setup.ts"], - include: ["tests/**/*.test.ts"], - testTimeout: 10000, - hookTimeout: 10000, - }, -}); \ No newline at end of file diff --git a/rag-gateway/vitest.integration.config.ts b/rag-gateway/vitest.integration.config.ts deleted file mode 100644 index c7a0ee9..0000000 --- a/rag-gateway/vitest.integration.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "node", - setupFiles: ["./tests/integration/setup.ts"], - include: ["tests/integration/**/*.test.ts"], - testTimeout: 30000, - hookTimeout: 10000, - }, -}); \ No newline at end of file diff --git a/skills-engineering/README.md b/skills-engineering/README.md index 9b69bd1..f7cac07 100644 --- a/skills-engineering/README.md +++ b/skills-engineering/README.md @@ -16,6 +16,8 @@ | `epistemic-integrity` | 全局技能 | 真值接地:反幻觉、验证方法论、求真边界 | | `logical-reasoning` | 全局技能 | 论证纪律:可追溯逻辑链、层级分明 | | `problem-analysis` | 全局技能 | 问题前置分析:逻辑检验、第一性原理拆解 | +| `plan-grill` | 工作流技能 | 需求对齐/盘问锁定计划,产出 PLAN.md(基于 grill-me) | +| `cross-model-review` | 工作流技能 | 跨模型对抗审查 PLAN.md,自动发现 CLI(基于 grill-me-codex) | 本仓库同时提供三类能力: diff --git a/skills-engineering/cross-model-review/AGENT-BRIEF.md b/skills-engineering/cross-model-review/AGENT-BRIEF.md new file mode 100644 index 0000000..0bbcd83 --- /dev/null +++ b/skills-engineering/cross-model-review/AGENT-BRIEF.md @@ -0,0 +1,29 @@ +# cross-model-review Agent 调用指南 + +## 一句话描述 + +自动发现可用 CLI(codex/gemini/claude),推荐两个不同 provider 的组合并让用户选择,reviewer 只读运行输出 VERDICT,原始输出和交付日志都写在当前项目根目录下,主 agent 仲裁写进 PLAN-REVIEW-LOG.md,MAX_ROUNDS 不收敛输出 deadlock。 + +## 何时调用 + +- **用户触发**:用户说 `cross-model-review` / `cross review` / "对抗审查" / "让两个模型审计划" / "model debate" / "stress-test PLAN.md" +- **接力 plan-grill**:plan-grill 锁定 PLAN.md 后,用户说"让另一个模型审查" +- **前置**:PLAN.md 必须已存在(由 plan-grill 产出) + +## 关键行为 + +1. 阅读 `SKILL.md` + `references/cross_model_review.md` 全文。 +2. 直接探测可用 CLI(`command -v codex|gemini|claude` + `--version`;本仓库可选辅助脚本:`skills-engineering/scripts/detect-review-clis.sh`)。可用 provider < 2 则停止。 +3. 推荐两个不同 provider 的组合,让用户确认(CMR-002)。 +4. reviewer 只读运行(CMR-003):codex `-s read-only`、gemini `--approval-mode plan`、claude `--permission-mode plan`。 +5. 输出落盘(CMR-003/004):reviewer 原始输出、中间输出和交付日志必须在当前项目根目录下,推荐 `.plan-reviews/-/raw/`;不得用 `/tmp` 作为 reviewer 输出缓冲。 +6. 主 agent 仲裁(CMR-004):每轮收集所有已选 reviewer;全部 APPROVED 才收敛,任一 REVISE 都要仲裁、修订并记入 PLAN-REVIEW-LOG.md。 +7. MAX_ROUNDS(默认 5)不收敛 → deadlock(CMR-005),交用户裁决,不假装 approved。 +8. Resolution 后可选归档(用户触发):把 PLAN.md + PLAN-REVIEW-LOG.md 保存到项目根 `.plan-reviews/-/`,供相似问题回查。详见 references「归档」章节。 + +## 不调用的情况 + +- 无 PLAN.md(先跑 plan-grill) +- trivial 改动 +- 用户明确"直接实施" +- 可用 reviewer provider < 2 diff --git a/skills-engineering/cross-model-review/OUT-OF-SCOPE.md b/skills-engineering/cross-model-review/OUT-OF-SCOPE.md new file mode 100644 index 0000000..b82e621 --- /dev/null +++ b/skills-engineering/cross-model-review/OUT-OF-SCOPE.md @@ -0,0 +1,15 @@ +# cross-model-review 范围外 + +本 skill 负责**已锁定计划的跨模型对抗审查**,不负责盘问锁定、问题审查、代码审查、计划执行。 + +## 不处理的内容 + +- **计划锁定**:盘问用户锁定 PLAN.md 是 `plan-grill`(PG-001~004)的职责。cross-model-review 只审查已锁定的 PLAN.md。 +- **问题审查**:问题本身的逻辑有效性、真实需求拆解由 `problem-analysis`(PA-001~003)负责,先于 plan-grill。 +- **已写代码的审查**:审查已实现的代码(而非计划)由 `ios-engineer/references/review_checklists.md` 处理。本 skill 只审查 PLAN.md。 +- **计划执行**:cross-model-review 只审查,不实施。实施由后续对话或 ios-engineer skill 承接。两幕期间不写任何代码。 +- **单模型审查**:本 skill 必须跨 provider。同 provider 审查(如 Claude 审 Claude 的计划)失去对抗价值,不予执行。 + +## 触发门控 + +仅在 PLAN.md 已存在时触发。若无 PLAN.md,先加载 plan-grill。 diff --git a/skills-engineering/cross-model-review/SKILL.md b/skills-engineering/cross-model-review/SKILL.md new file mode 100644 index 0000000..2dbd13e --- /dev/null +++ b/skills-engineering/cross-model-review/SKILL.md @@ -0,0 +1,38 @@ +--- +name: cross-model-review +description: 跨模型对抗审查已锁定的 PLAN.md——自动发现可用 CLI(codex/gemini/claude),推荐两个不同 provider 的组合并让用户选择,reviewer 只读运行输出 VERDICT:APPROVED|REVISE,主 agent 仲裁并把理由写进 PLAN-REVIEW-LOG.md,MAX_ROUNDS 不收敛输出 deadlock。基于 chaseai-yt/grill-me-codex(MIT)的 Act 2 思路。 +--- + +# Cross Model Review + +## 强制入口 + +命中本 skill 时,**必须先完整阅读** [references/cross_model_review.md](references/cross_model_review.md) 并按其中条款执行。 + +- 不得以 preamble、Cursor 规则摘要或其它二次摘要代替该文件全文。 +- 本 skill 是 `plan-grill` 的 Act 2;plan-grill 锁定 PLAN.md 后接力本 skill。 + +## 五条核心规则 + +- [CMR-001] **自动发现 reviewer**:直接探测 codex/gemini/claude 三个 CLI 的可用性、版本、non-interactive 与只读模式支持(可用 `command -v ` + ` --version`,本仓库可选辅助脚本为 `skills-engineering/scripts/detect-review-clis.sh`)。可用 provider < 2 时停止并提示安装,不伪造 cross-model。 +- [CMR-002] **推荐组合 + 用户选择**:从可用 CLI 中推荐两个不同 provider 的组合(如 codex + gemini),让用户确认。不静默替用户选死。 +- [CMR-003] **reviewer 只读**:每个 reviewer 必须以只读模式运行——codex 用 `-s read-only`,gemini 用 `--approval-mode plan`,claude 用 `--permission-mode plan`。reviewer 不写代码,只输出 `VERDICT: APPROVED` 或 `VERDICT: REVISE` + 具体修改建议。 +- [CMR-004] **主 agent 仲裁**:主 agent(Claude/Codex,视宿主而定)是最终仲裁者。每轮必须收集所有已选 reviewer 的 verdict;reviewer 原始输出、中间输出和交付日志必须保存在当前项目根目录下(推荐 `.plan-reviews/-/raw/`),不得用 `/tmp` 作为 reviewer 输出缓冲。只有全部 `APPROVED` 才能收敛,任一 `REVISE` 都必须仲裁并进入修订/下一轮。采纳有证据的批评,拒绝不成立的批评并写明理由,记录进 `PLAN-REVIEW-LOG.md`。 +- [CMR-005] **MAX_ROUNDS + deadlock**:到 MAX_ROUNDS(默认 5)仍不收敛时,输出 deadlock——列出每个未决点 + 主 agent 的反立场,交给用户裁决。禁止假装 approved。 + +细则见 [references/cross_model_review.md](references/cross_model_review.md)。 + +## 何时加载 + +- **默认触发**:用户说 `cross-model-review` / `cross review` / "对抗审查" / "让两个模型审计划" / "model debate" / "stress-test PLAN.md" / "review PLAN.md" / "让 Gemini/Codex/Claude 审一下计划"。 +- **接力 plan-grill**:plan-grill 锁定 PLAN.md 后,用户说"让另一个模型审查"则加载本 skill。 +- **跳过**:没有 PLAN.md(先跑 plan-grill);trivial 改动;用户明确"直接实施"。 + +## 与相邻 skill 的分工 + +| Skill | 分工 | +|-------|------| +| `plan-grill`(PG-001~004) | 盘问锁定 PLAN.md(Act 1) | +| **cross-model-review(本 skill)** | 跨模型对抗审查 PLAN.md(Act 2) | +| `problem-analysis`(PA-001~003) | 问题审查,先于 plan-grill | +| `epistemic-integrity`(GR-011~013) | 主 agent 仲裁时的真值接地纪律 | diff --git a/skills-engineering/cross-model-review/examples/regression-login-rate-limit.md b/skills-engineering/cross-model-review/examples/regression-login-rate-limit.md new file mode 100644 index 0000000..effd701 --- /dev/null +++ b/skills-engineering/cross-model-review/examples/regression-login-rate-limit.md @@ -0,0 +1,263 @@ +# 回归场景:cross-model-review × 用户登录速率限制 + +> 本文件是 cross-model-review skill 的回归场景样例,供人类阅读,不参与 Agent 运行时加载(sync-skills.sh 白名单不同步 examples/)。 + +## 场景目的 + +验证 cross-model-review skill 的端到端流程: + +1. `detect-review-clis.sh` 探测可用 CLI +2. 推荐两个不同 provider 的组合 +3. reviewer 只读审查 PLAN.md +4. 输出 `VERDICT: APPROVED | REVISE` +5. 主 agent 仲裁并写 PLAN-REVIEW-LOG.md +6. reviewer 原始输出保存在当前项目根 `.plan-reviews/-/raw/`,不使用 `/tmp` + +## 前置 + +plan-grill 盘问后的 PLAN.md(用户登录速率限制方案)。盘问修正了原方案的 4 个缺陷:单实例假设、并发竞态、时钟重置、锁定恢复。 + +## PLAN.md(盘问锁定版) + +```markdown +# Plan: 用户登录速率限制(防暴力破解) + +## Goal +防止暴力破解登录,限制每个用户/IP 的登录失败次数,超限后临时锁定。 + +## Constraints & assumptions +- Web 服务多实例部署(不能依赖单实例内存) +- Redis 可用作共享存储 +- 时间精度到秒即可 + +## Approach +用 Redis 存储失败计数。为 username 和 IP 各维护一个 key(分开,非组合),任一超限即触发锁定。每次登录失败用 INCR 原子递增,首次失败时用 EXPIRE 设置 15 分钟 TTL(固定窗口自动重置)。达到 5 次后,设置一个 15 分钟的锁定 key。锁定期间登录请求直接拒绝并返回 Retry-After 头。锁定结束后 key 过期自动恢复。 + +## Key decisions & tradeoffs +- 分开两个 key(`loginfail:u:{username}` + `loginfail:ip:{ip}`)而非组合 key:能同时卡住"同账号不同 IP"和"同 IP 不同账号"两种暴破模式;代价是 key 数量翻倍,但 Redis 可承受。 +- 固定窗口而非滑动窗口:防暴破不需要精确计量,边界处 2x 突发可接受;换来实现简单(TTL 自动重置)。 +- 阈值 5 次/锁定 15 分钟:OWASP 建议的暴破防护下限;15 分钟让攻击成本升高又不致过度惩罚合法用户。 +- Redis INCR + EXPIRE 原子操作:天然解决并发竞态,无需额外加锁。 +- 不提供提前解锁接口:避免被攻击者社工绕过。 + +## Validation plan +- 单元测试:模拟 5 次失败后第 6 次被拒;锁定 key TTL 正确;username 和 IP 两个维度独立计数。 +- 并发测试:100 并发失败请求,计数器不丢不重(验证 INCR 原子性)。 +- 集成测试:多实例部署下,实例 A 的失败在实例 B 可见(验证 Redis 共享)。 +- 边界测试:窗口边界(14:59 五次 + 15:01 五次)行为符合固定窗口预期。 + +## Risks / non-blocking open questions +- Redis 故障时如何降级:可选 fail-open(放行,记日志)或 fail-closed(拒绝所有登录)。当前未决,建议 fail-open + 告警,避免 Redis 故障导致全员无法登录。 + +## Out of scope +- 验证码/二次认证(本方案只做速率限制) +- 分布式 IP 信誉库 +- 异常检测(如地理跃迁检测) +``` + +## detect-review-clis.sh 输出 + +```json +{ + "clis": [ + {"name": "codex", "available": false}, + {"name": "gemini", "available": true, "path": "/opt/homebrew/bin/gemini", "version": "0.49.0", "readonly_flag": "--approval-mode plan", "noninteractive_flag": "-p"}, + {"name": "claude", "available": true, "path": "/Users/you/.local/bin/claude", "version": "2.1.195 (Claude Code)", "readonly_flag": "--permission-mode plan", "noninteractive_flag": "-p"} + ], + "available_count": 2 +} +``` + +`available_count >= 2`,满足 CMR-001 硬门。推荐组合:gemini + claude(两个不同 provider)。 + +## Reviewer 调用命令 + +```bash +REVIEW_DIR="./.plan-reviews/2026-07-06-login-rate-limit" +RAW_DIR="${REVIEW_DIR}/raw" +mkdir -p "${RAW_DIR}" +grep -qxF ".plan-reviews/" .gitignore 2>/dev/null || printf "\n.plan-reviews/\n" >> .gitignore + +# Reviewer 1: Claude +claude -p "$REVIEW_PROMPT" --permission-mode plan --output-format text \ + > "${RAW_DIR}/claude-round1.txt" + +# Reviewer 2: Gemini +gemini -p "$REVIEW_PROMPT" --approval-mode plan --skip-trust --include-directories ~/.gemini/skills \ + > "${RAW_DIR}/gemini-round1.txt" +``` + +其中 `REVIEW_PROMPT`(见详规 CMR-003): + +``` +You are an adversarial reviewer for an implementation plan. Be skeptical and specific — find what breaks, not to be agreeable. Identify concrete flaws: security holes, race conditions, missing edge cases, schema conflicts, wrong assumptions, observability gaps, simpler alternatives. For each, give a one-line fix. Do NOT modify any files. End your reply with EXACTLY one line: VERDICT: APPROVED if the plan is sound enough to implement, or VERDICT: REVISE if it still has material problems. +``` + +## Reviewer 1: Claude(8 个缺陷,VERDICT: REVISE) + +**Flaw 1 — INCR + EXPIRE are NOT atomic (critical correctness bug)** + +计划声称"INCR + EXPIRE 原子操作"——这是事实性错误。它们是两条独立 Redis 命令。若进程在 INCR 返回 1 后、EXPIRE 发送前崩溃,计数器 key 永久存在无 TTL。5 次失败后账号永久锁定,无自动恢复。 + +Fix: 用 Lua 脚本(EVAL)封装 GET → INCR → 条件 EXPIRE,Redis 保证 Lua 脚本原子性。 + +**Flaw 2 — Username-dimension key enables targeted account lockout (DoS)** + +按 username 分开意味着任何未认证攻击者知道合法用户名即可用轮换 IP 的 5 次请求锁定它。这是可武器化的用户 DoS:攻击者每 15 分钟锁 `admin@company.com`,阻止目标登录。 + +Fix: 用组合 key `loginfail:u:{username}:ip:{ip}`(只锁配对),或保留 per-username 限制但在阈值 3 加 CAPTCHA,让锁定成本落在攻击者 UX 而非受害者。 + +**Flaw 3 — IP is not a trustworthy input (injection/bypass)** + +计划用 IP 作 key 但未指定如何获取。负载均衡器后 `REMOTE_ADDR` 是代理 IP,所有用户共享一个计数器。若读 `X-Forwarded-For`,攻击者设置 `X-Forwarded-For: 1.2.3.4` 轮换 header 值绕过。 + +Fix: 只从受控代理设置的可信 header 提取 IP(如 nginx `X-Real-IP` + `set_real_ip_from`),绝不直接读用户提供的 header。 + +**Flaw 4 — No counter reset on successful authentication** + +4 次失败后成功登录,再 1 次失败即被锁。失败计数器在成功时从不清除,跨会话累积。 + +Fix: 成功认证时 `DEL loginfail:u:{username}`(及 IP key)重置计数器。 + +**Flaw 5 — Retry-After header value is wrong after partial lock time** + +锁 key 10 分钟前设置,剩 5 分钟,但响应 `Retry-After: 900` 告诉客户端等 15 分钟。 + +Fix: 响应 `Retry-After: PTTL(lock_key) / 1000`(剩余 TTL 秒),非硬编码 900。 + +**Flaw 6 — After lock expiry, counter is still at 5 (immediate re-lock)** + +计数器 key 和锁 key 在不同时间设置(失败 1 vs 失败 5)。锁过期后,计数器可能仍是 5,用户下一次失败立即重新锁定。 + +Fix: 设置锁 key 时同时 `DEL` 计数器 key,窗口干净重置。 + +**Flaw 7 — Username key is not normalized (trivial bypass)** + +`loginfail:u:Admin` 和 `loginfail:u:admin` 是不同 key。若用户名大小写不敏感,攻击者轮换大小写变体获得 5×N 次尝试。 + +Fix: 构造 key 前规范化用户名为小写(或规范形式)。 + +**Flaw 8 — No observability when locks trigger** + +验证计划覆盖正确性测试,但完全没提结构化日志或 metrics。安全事件(账号锁定、IP 封禁)必须可观测,用于威胁检测、事件响应、调优。 + +Fix: 每次写锁 key 时发结构化日志事件 `{"event":"login_locked","reason":"username|ip","key":"...","ts":...}`。 + +**VERDICT: REVISE** + +## Reviewer 2: Gemini(5 个缺陷,VERDICT: REVISE) + +**缺陷 1: 账号恶意锁定拒绝服务漏洞** + +允许仅通过 username 锁定用户登录会把"暴力破解风险"转化为"拒绝服务风险"。 + +Fix: 账号维度超限不应直接拒绝,而应强制触发验证码、MFA 或仅对"IP+Username"联合 key 拒绝。 + +**缺陷 2: INCR 与 EXPIRE 之间非原子性导致的死锁** + +INCR 返回 1 后、EXPIRE 执行前若网络中断/崩溃/主从切换,计数器 key 永久存在(TTL = -1)。 + +Fix: Lua 脚本打包 `local c = redis.call('incr', KEYS[1]); if c == 1 then redis.call('expire', KEYS[1], ARGV[1]) end; return c`。 + +**缺陷 3: 登录成功时未重置计数器** + +用户 15 分钟内错 4 次后第 5 次成功登录,计数器仍是 4,后续错 1 次即被锁。 + +Fix: 成功后同步/异步 `DEL loginfail:u:{username}` 和 `DEL loginfail:ip:{ip}`。 + +**缺陷 4: 反向代理下的真实 IP 伪造与误锁风险** + +直接读 Socket IP 会误锁反向代理本身;直接信任 `X-Forwarded-For` 会被伪造绕过。 + +Fix: 配置受信任代理 IP 列表,只从受信代理传递的 `X-Forwarded-For` 最左端提取真实 IP。 + +**缺陷 5: 观测性缺失** + +无 metrics 和 audit log,管理员无法感知暴破攻击,无法对被恶意锁定的正常用户排查。 + +Fix: 锁定触发时输出包含模糊化用户名、客户端 IP、拦截维度、TTL 的安全审计日志,并增加锁定触发率突增报警。 + +**VERDICT: REVISE** + +## 缺陷重叠分析 + +| # | 缺陷 | Claude | Gemini | 严重度 | 置信度 | +|---|------|:---:|:---:|---|---| +| 1 | INCR + EXPIRE 非原子(永久锁定) | ✅ | ✅ | Critical | 高(两者都发现) | +| 2 | Username 维度导致用户 DoS | ✅ | ✅ | High | 高 | +| 3 | IP 伪造(X-Forwarded-For) | ✅ | ✅ | High | 高 | +| 4 | 登录成功未重置计数器 | ✅ | ✅ | Medium | 高 | +| 5 | 观测性缺失(无日志/metrics) | ✅ | ✅ | Low | 高 | +| 6 | Retry-After 值错误(硬编码 900) | ✅ | — | Medium | 中(仅 Claude) | +| 7 | 锁定过期后立即重新锁定 | ✅ | — | Medium | 中 | +| 8 | Username 未规范化(大小写绕过) | ✅ | — | Low | 中 | + +5 个缺陷**两者都发现**(高置信),3 个仅 Claude 发现(Claude 审查更细)。不同 provider 结构盲区不同,叠加覆盖更全——这正是跨模型对抗的价值。 + +## 主 agent 仲裁示例(PLAN-REVIEW-LOG.md) + +```markdown +# Plan Review Log: 用户登录速率限制(防暴力破解) + +MAX_ROUNDS=5 +Reviewers: +- claude 2.1.195 (Anthropic) +- gemini 0.49.0 (Google) + +## Round 1 - claude +[8 个缺陷,详见上文 Reviewer 1] +VERDICT: REVISE + +### Orchestrator response +Accepted: +- Flaw 1 (INCR+EXPIRE 非原子): 采纳,改用 Lua 脚本封装。这修正了主 agent 在 plan-grill 盘问时的错误断言——"INCR + EXPIRE 原子操作"是事实性错误。 +- Flaw 2 (用户 DoS): 采纳,改用组合 key `loginfail:u:{username}:ip:{ip}`,只锁配对。 +- Flaw 3 (IP 伪造): 采纳,明确只从受信代理 header 提取 IP。 +- Flaw 4 (成功未重置): 采纳,成功时 DEL 计数器。 +- Flaw 6 (Retry-After 值): 采纳,改用 PTTL 返回剩余秒数。 +- Flaw 7 (锁定后立即重锁): 采纳,设锁 key 时 DEL 计数器。 +- Flaw 8 (Username 规范化): 采纳,key 前小写化。 +- Flaw 8 (观测性): 采纳,加结构化日志 + metrics。 + +Rejected: +- (无) 本轮所有批评均有证据,全部采纳。 + +## Round 1 - gemini +[5 个缺陷,详见上文 Reviewer 2] +VERDICT: REVISE + +### Orchestrator response +Accepted: +- 缺陷 1-5: 与 Claude 重叠,已在上文 claude 轮采纳。无新增。 + +Rejected: +- (无) + +## Resolution +deadlock(本轮 REVISE,需修订 PLAN.md 后进入 Round 2;本样例到此为止,不演示完整收敛) +``` + +## 验证结论 + +| 验证项 | 结果 | +|---|---| +| `detect-review-clis.sh` 探测 | ✅ 正确识别 gemini + claude 可用 | +| claude adapter 调用 | ✅ 输出完整审查 + VERDICT: REVISE | +| gemini adapter 调用 | ✅ 输出完整审查 + VERDICT: REVISE | +| 项目内 raw 输出 | ✅ reviewer 原始输出保存到 `.plan-reviews/2026-07-06-login-rate-limit/raw/` | +| VERDICT 格式 | ✅ 两 reviewer 均以 VERDICT: REVISE 结尾 | +| 跨模型重叠 | ✅ 5 个缺陷两者都发现(高置信) | +| 主 agent 盲区被抓 | ✅ INCR+EXPIRE 非原子——主 agent 在 plan-grill 盘问时错误断言"原子操作",两 reviewer 都抓到 | + +### 核心价值证明 + +plan-grill 盘问修正了原方案的 4 个缺陷,但**遗漏了** INCR+EXPIRE 非原子这个结构性错误(主 agent 自信地断言错了)。cross-model-review 的跨模型对抗**抓到了**这个盲区。 + +这证明了流水线的核心价值:**不能让同一模型既规划又评分**——回音室效应会让错误自信通过。跨提供商模型对抗是发现结构性盲区的有效机制。 + +## gemini adapter 调用注意事项(本场景观察) + +- gemini 启动时报 `ContextManager Hot start calibration failed` 503(context-calibrator 模型在第三方 API 中转下不可用),但**不阻止** reviewer 输出完整审查。 +- 加 `--include-directories ~/.gemini/skills` 可消除 `Path not in workspace` 噪音。 + +详见详规 `references/cross_model_review.md` 的「Gemini adapter → 调用注意事项」。 diff --git a/skills-engineering/cross-model-review/references/cross_model_review.md b/skills-engineering/cross-model-review/references/cross_model_review.md new file mode 100644 index 0000000..88d0b6c --- /dev/null +++ b/skills-engineering/cross-model-review/references/cross_model_review.md @@ -0,0 +1,291 @@ + +# 跨模型对抗审查(Cross Model Review) + +> **真值来源**:本文件为唯一详规正文。`cross-model-review/SKILL.md` 为入口;各端完整副本由 `scripts/sync-skills.sh` 同步;Cursor 项目内由 `sync-agent-preamble.sh` 生成 `.cursor/rules/cross-model-review.mdc`。 + +## 定位 + +cross-model-review 解决 AI 辅助编码的第 2 类失败模式:**计划听起来对,但会崩**。同一个模型既规划又评分无法发现自己的结构盲区——必须靠**跨提供商模型**对抗审查。 + +本 skill 基于 `chaseai-yt/grill-me-codex`(MIT 许可)的 Act 2 思路,适配本项目多 adapter 架构。 + +## 前置 + +- `PLAN.md` 必须已由 `plan-grill` 锁定(PG-004 产出)。无 PLAN.md 时先跑 plan-grill。 +- 当前环境至少有两个不同 provider 的 reviewer CLI 可用(CMR-001 探测)。 + +## CMR-001 自动发现 reviewer + +直接探测当前环境中的 reviewer CLI。安装后的 skill 不依赖仓库级脚本;在 `ai-coding-kit` 仓库内可选运行辅助脚本: + +```bash +bash skills-engineering/scripts/detect-review-clis.sh +``` + +通用探测方法: + +```bash +command -v codex >/dev/null 2>&1 && codex --version +command -v gemini >/dev/null 2>&1 && gemini --version +command -v claude >/dev/null 2>&1 && claude --version +``` + +把探测结果整理为等价 JSON(人工整理即可,不要求生成文件): + +```json +{ + "clis": [ + {"name":"codex","available":true,"path":"...","version":"0.142.5","readonly_flag":"-s read-only","noninteractive_flag":"exec"}, + {"name":"gemini","available":true,"path":"...","version":"0.49.0","readonly_flag":"--approval-mode plan","noninteractive_flag":"-p"}, + {"name":"claude","available":false} + ], + "available_count": 2 +} +``` + +**硬门**:可用 provider 数 < 2 时**停止**,提示用户安装缺失 CLI,不伪造 cross-model。跨模型对抗至少需要两个不同 provider 的 CLI。 + +## CMR-002 推荐组合 + 用户选择 + +从可用 CLI 中推荐两个**不同 provider** 的组合: + +| 主 agent | 推荐 reviewer 组合 | +|---|---| +| Claude(宿主为 Claude Code) | codex + gemini(避开 Anthropic) | +| Codex(宿主为 Codex CLI) | gemini + claude(避开 OpenAI) | + +向用户呈现候选表,让用户确认或调整: + +``` +检测到可用 reviewer: +1. codex 0.142.5 (OpenAI) 只读: -s read-only +2. gemini 0.49.0 (Google) 只读: --approval-mode plan + +推荐组合:codex + gemini(两个不同 provider) +确认?或指定另外两个 reviewer? +``` + +**不静默替用户选死**。用户确认后才进入审查。若用户只指定一个 reviewer,必须说明这会降级为普通单模型审查,不再使用 `cross-model-review` 流程。 + +## CMR-003 reviewer 只读(三 adapter 调用命令) + +每个 reviewer 必须以只读模式运行。reviewer 不写代码,只读 PLAN.md 和相关 repo 文件,输出 `VERDICT: APPROVED` 或 `VERDICT: REVISE` + 具体修改建议。 + +### 项目内输出目录(强制) + +reviewer 原始输出、中间输出、交付日志都必须保存在**当前项目根目录**下,禁止把 `/tmp` 作为 reviewer 输出缓冲。推荐在审查开始前创建: + +```bash +REVIEW_SLUG="" +REVIEW_DIR="./.plan-reviews/${REVIEW_SLUG}" +RAW_DIR="${REVIEW_DIR}/raw" +mkdir -p "${RAW_DIR}" +grep -qxF ".plan-reviews/" .gitignore 2>/dev/null || printf "\n.plan-reviews/\n" >> .gitignore +``` + +规则: + +- `PLAN.md` 和 `PLAN-REVIEW-LOG.md` 是当前项目根的交付物。 +- 若 PLAN.md 引用了 `.plan-reviews//architecture-analysis.md`,reviewer 必须把它作为只读输入,与 PLAN.md 一起审查。 +- reviewer 原始输出写入 `${RAW_DIR}/-round.`。 +- 可选归档时,把项目根 `PLAN.md` / `PLAN-REVIEW-LOG.md` 同步到 `${REVIEW_DIR}/`,`raw/` 原样保留。 +- 创建 `.plan-reviews/` 时,默认把 `.plan-reviews/` 追加到当前项目根 `.gitignore`;审查证据默认是本地工作产物,除非用户明确要求纳入版本控制。 +- `/tmp` 只允许用于与本流程无关的普通一次性 shell scratch;不得用于保存 reviewer verdict、critique、thread/session id 或任何需要审计的 cross-model-review 证据。 + +### 审查 prompt(每轮发送给 reviewer) + +``` +You are an adversarial reviewer for an implementation plan. Be skeptical and specific — your job is to find what breaks, not to be agreeable. Read the plan at PLAN.md, any architecture-analysis.md file referenced by PLAN.md, and any repo files you need (you are read-only). Identify concrete flaws: security holes, race conditions, missing edge cases, schema conflicts, wrong assumptions, observability gaps, simpler alternatives. For each, give a one-line fix. Do NOT modify any files. End your reply with EXACTLY one line: `VERDICT: APPROVED` if the plan is sound enough to implement, or `VERDICT: REVISE` if it still has material problems. +``` + +### Codex adapter + +```bash +# Round 1 — 全新会话,拿 thread_id +codex exec -s read-only --json -o "${RAW_DIR}/codex-round1.json" "$REVIEW_PROMPT" \ + < /dev/null 2>/dev/null | grep '"type":"thread.started"' + +# Round 2+ — 续接同一会话(Codex 记得之前的批评) +codex exec resume "$THREAD_ID" -c sandbox_mode="read-only" --json \ + -o "${RAW_DIR}/codex-round${ROUND}.json" \ + "I revised the plan. Re-review PLAN.md — check whether your prior findings are addressed and flag anything new. End with VERDICT: APPROVED or VERDICT: REVISE." \ + < /dev/null 2>/dev/null >/dev/null +``` + +**关键**: +- `< /dev/null` 强制必需——`codex exec` 在非交互式下会读 stdin,不重定向会永久 hang。 +- `resume` 不支持 `-s`,必须用 `-c sandbox_mode="read-only"` 强制只读。 +- timeout 600s 守卫(详见安全规则)。 + +### Gemini adapter + +```bash +# Round 1 — 全新会话 +gemini -p "$REVIEW_PROMPT" --approval-mode plan -o json --skip-trust \ + > "${RAW_DIR}/gemini-round1.json" + +# Round 2+ — 续接同一会话 +gemini -r "$SESSION_ID" -p "$RESUME_PROMPT" --approval-mode plan -o json \ + > "${RAW_DIR}/gemini-round${ROUND}.json" +``` + +**关键**:`--approval-mode plan` 是只读模式;`-r/--resume` 支持 `latest` 或 session index。 + +**调用注意事项**: + +1. **preamble 与 workspace**:gemini 启动会加载全局 `~/.gemini/GEMINI.md`(本项目 `sync-agent-preamble.sh` 写入),preamble 要求读 `~/.gemini/skills/` 下文件,但 workspace 限制可能拒绝 → 产生 `Error executing tool read_file: Path not in workspace` 噪音。这不阻塞 reviewer 主流程。 + - 缓解:调用时加 `--include-directories ~/.gemini/skills` 消除噪音。 +2. **context-calibrator**:若 `GEMINI_API_KEY` 是第三方中转(非 Google 官方),可能不支持 `context-calibrator` 模型 → `Hot start calibration failed` 503。此错误是噪音,不阻止 reviewer 输出 VERDICT。 + +### Claude adapter + +```bash +# Round 1 — 全新会话 +claude -p "$REVIEW_PROMPT" --permission-mode plan --output-format json \ + > "${RAW_DIR}/claude-round1.json" + +# Round 2+ — 续接同一会话(resume 参数以 claude --help 为准) +claude --resume "$SESSION_ID" -p "$RESUME_PROMPT" --permission-mode plan --output-format json \ + > "${RAW_DIR}/claude-round${ROUND}.json" +``` + +> Claude adapter 的 resume 参数需以实际 `claude --help` 为准验证;第一版若 resume 不可用,可降级为每轮传完整 conversation history(messages 数组)。 + +### 第一版不 pin model + +每个 adapter 使用 CLI 配置里的**默认模型**。只有用户显式指定时才传 `--model`。这与 Chase 上游"不轻易 pin model"的安全经验一致——pin `gpt-5.x-codex` 变体在 ChatGPT 账户鉴权下会 400。 + +## CMR-004 主 agent 仲裁 + +每轮 reviewer 返回后: + +1. 对本轮所有已选 reviewer 都完成调用,读取项目内 raw 文件(`${RAW_DIR}/-round.`)。 +2. 逐个追加到项目根 `PLAN-REVIEW-LOG.md`:`## Round - ` + 完整 critique + raw 文件相对路径。 +3. 汇总本轮 verdict: + - **全部** reviewer 都返回 `VERDICT: APPROVED` → 才能进入 Resolution(收敛)。 + - **任一** reviewer 返回 `VERDICT: REVISE` → 主 agent 决定**哪些值得采纳**。修订 `PLAN.md`。追加 `### Orchestrator response` 到 LOG:Accepted / Rejected + 理由。进入下一轮。 + - 任一 reviewer 未输出合法 verdict → 本轮失败,停止并告知用户,不把缺失 verdict 当作 approval。 +4. **仲裁纪律**: + - 采纳有证据的批评(具体到代码/假设/边界)。 + - 拒绝不成立的批评,写明理由(如"reviewer 误读了 X,实际是 Y")。 + - 不盲从(否则失去仲裁价值),不无视(否则失去对抗价值)。 + +## CMR-005 MAX_ROUNDS + deadlock + +| 参数 | 默认 | 含义 | +|---|---|---| +| `MAX_ROUNDS` | `5` | 硬上限。循环在此终止。 | +| `PLAN_FILE` | `PLAN.md` | plan-grill 产出的计划。 | +| `LOG_FILE` | `PLAN-REVIEW-LOG.md` | 追加式论证记录,是交付物。 | +| `RAW_DIR` | `.plan-reviews/-/raw` | reviewer 原始输出目录,必须在当前项目根内。 | + +若调用时传 `rounds=3`,用该值覆盖 `MAX_ROUNDS`。启动前 echo 已解析的值。 + +### Resolution(用户最终签署) + +- **APPROVED**:呈现最终 PLAN.md + 3 条改进摘要 + 轮数。问:"经 N 轮跨模型审查。现在实施?" 仅在用户同意后写代码。**两幕期间不写任何代码。** +- **deadlock(MAX_ROUNDS 用尽未 APPROVED)**:**禁止假装 approved**。列出每个未决点 + 主 agent 的反立场,交给用户裁决。一个标记清楚的分歧胜过一个虚假的"已批准"。 + +## 归档(可选,用户触发) + +审查完成后,主 agent 提示用户是否归档。归档把 PLAN.md + PLAN-REVIEW-LOG.md 保存到**当前项目根**的 `.plan-reviews/` 下,供后续相似问题回查——"上次设计速率限制时问了哪些问题?发现过什么缺陷?" + +### 归档目录结构 + +``` +/.plan-reviews/ +└── -/ + ├── PLAN.md # plan-grill 锁定的计划 + ├── PLAN-REVIEW-LOG.md # cross-model-review 完整论证记录 + ├── architecture-analysis.md # 可选,PG-005 快速架构分析 + ├── raw/ # reviewer 原始输出(每轮每 reviewer 一个文件) + │ ├── claude-round1.json + │ └── gemini-round1.json + └── SUMMARY.md # 可选,人工整理的总结 +``` + +### 触发流程 + +1. Resolution 后(APPROVED 或 deadlock),主 agent 提示:「审查完成。是否归档到 `./.plan-reviews/-/`?可先编辑 PLAN.md / PLAN-REVIEW-LOG.md 后再保存。」 +2. 用户提供 slug(如 `login-rate-limit`)。 +3. 主 agent 执行: + + ```bash + ARCHIVE_DIR="./.plan-reviews/$(date +%Y-%m-%d)-${SLUG}" + mkdir -p "${ARCHIVE_DIR}/raw" + grep -qxF ".plan-reviews/" .gitignore 2>/dev/null || printf "\n.plan-reviews/\n" >> .gitignore + cp PLAN.md PLAN-REVIEW-LOG.md "${ARCHIVE_DIR}/" + # 若 PLAN.md 引用了 PG-005 架构分析文件,也复制为 "${ARCHIVE_DIR}/architecture-analysis.md"。 + ``` + +4. 用户可选写 `SUMMARY.md`(人工整理:关键盘问问题、发现的缺陷、修复要点)。 + +### 归档原则 + +- **按项目保存**:归档到当前项目根的 `.plan-reviews/`,不进 skill 仓库;不同项目各自独立。reviewer raw 输出也属于审计证据,必须保留在该目录的 `raw/` 下。 +- **默认忽略**:创建 `.plan-reviews/` 时必须默认把 `.plan-reviews/` 写入当前项目根 `.gitignore`。若团队确实要共享审查归档,应由用户显式移除 ignore 或选择性复制整理后的摘要文件。 +- **人工整理**:归档前用户可编辑 PLAN.md / PLAN-REVIEW-LOG.md,删减噪音、补总结;不是机械保存。 +- **目的**:知识沉淀,相似问题回查。 +- **提交边界**:默认不提交 `.plan-reviews/`;如果要提交,优先提交人工整理后的 `SUMMARY.md` 或脱敏后的归档,而不是 raw reviewer 输出。 + +### 不归档的情况 + +- trivial 审查(无学习价值) +- 用户明确"不归档" +- 敏感项目(PLAN.md 含业务逻辑,不宜留痕) + +## PLAN-REVIEW-LOG.md 格式 + +```markdown +# Plan Review Log: + +MAX_ROUNDS=<n> +Reviewers: +- <cli/model A> +- <cli/model B> + +## Round 1 - <reviewer> +<critique> +VERDICT: REVISE + +### Orchestrator response +Accepted: +- <采纳点 1> +Rejected: +- <拒绝点 1> because <理由> + +## Resolution +<approved | deadlock> +``` + +## 安全规则 + +1. **reviewer 每轮只读**——codex `-s read-only` / resume `-c sandbox_mode="read-only"`;gemini `--approval-mode plan`;claude `--permission-mode plan`。reviewer 永远不写文件。 +2. **`< /dev/null` 必需**(codex)——非交互式下 stdin 不重定向会永久 hang(0% CPU 静默卡死)。 +3. **禁止 `/tmp` reviewer 缓冲**——reviewer 原始输出、verdict、critique、thread/session id、PLAN-REVIEW-LOG 都必须写在当前项目根下,推荐 `.plan-reviews/<date>-<slug>/raw/`;否则审计链不可复现。 +4. **timeout 600s 守卫**——每个 reviewer 调用加 10 分钟上限。Claude Code 的 Bash tool 传 `timeout: 600000`;纯 shell 用 `timeout 600`(Linux)或 `gtimeout 600`(macOS coreutils)。超时视为失败,停止并告知用户,不盲重试。 +5. **不 pin model**——用 CLI 默认模型,除非用户显式指定。 +6. **循环必在 MAX_ROUNDS 终止**——硬上限,不无限循环。 +7. **deadlock 不假装 approved**——未收敛时如实标记,交用户裁决。 + +## 跳过条件 + +- 无 PLAN.md(先跑 plan-grill) +- trivial 改动(不需要跨模型审查) +- 用户明确"直接实施" +- 可用 reviewer provider < 2(CMR-001 硬门) + +## 仲裁质量自检 + +审查结束前过一遍: + +- [ ] 每个 REVISE 是否都有 Accepted 或 Rejected 记录? +- [ ] Rejected 是否都写明理由? +- [ ] 是否同一轮全部 reviewer 都 APPROVED 后才进入 Resolution? +- [ ] reviewer raw 输出是否全部保存在当前项目根下(如 `.plan-reviews/<date>-<slug>/raw/`),没有使用 `/tmp`? +- [ ] PLAN-REVIEW-LOG.md 是否完整保留所有轮次论证? +- [ ] deadlock 时是否如实标记,未假装 approved? + +## 致谢 + +本 skill 基于 `chaseai-yt/grill-me-codex`(MIT 许可,https://github.com/chaseai-yt/grill-me-codex)的 Act 2 跨模型对抗审查机制。Codex adapter 的调用命令(`codex exec -s read-only`、`resume -c sandbox_mode`、`< /dev/null` 防 hang、timeout 600s)直接源自上游验证(2026-06-04)。扩展为三 adapter(codex/gemini/claude)自动发现架构。 diff --git a/skills-engineering/ios-engineer/evolution/approvals/20260706-163822-add-plan-grill-architecture-analysis-mode.json b/skills-engineering/ios-engineer/evolution/approvals/20260706-163822-add-plan-grill-architecture-analysis-mode.json new file mode 100644 index 0000000..e4c14d8 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/approvals/20260706-163822-add-plan-grill-architecture-analysis-mode.json @@ -0,0 +1,18 @@ +{ + "proposal_id": "20260706-163822-add-plan-grill-architecture-analysis-mode", + "approved_at": "2026-07-06T16:38:00+0800", + "active_version": "v73", + "validation": { + "structural": true, + "scenario": false, + "risk": "low" + }, + "changes": [ + { + "file": "skills-engineering/ios-engineer/references/architecture_analysis.md", + "type": "enhancement", + "description": "Add quick architecture analysis mode for plan-grill PG-005 delegation, including output format, discipline constraints, and differentiation from full Phase 1-4 flow" + } + ], + "status": "approved" +} diff --git a/skills-engineering/ios-engineer/evolution/proposals/20260706-163822-add-plan-grill-architecture-analysis-mode.md b/skills-engineering/ios-engineer/evolution/proposals/20260706-163822-add-plan-grill-architecture-analysis-mode.md new file mode 100644 index 0000000..551d671 --- /dev/null +++ b/skills-engineering/ios-engineer/evolution/proposals/20260706-163822-add-plan-grill-architecture-analysis-mode.md @@ -0,0 +1,31 @@ +# Skill Evolution Proposal + +## Metadata +- Proposal ID: 20260706-163822-add-plan-grill-architecture-analysis-mode +- Created At: 2026-07-06 16:38:22 +0800 +- Active Version At Creation: v73 + +## 问题信号 +- plan-grill PG-005 盘问中涉及少量文件(通常 ≤10 个)的跨文件依赖关系时,需要委托 ios-engineer 做快速架构分析;但现有 `architecture_analysis.md` 只有完整 Phase 1-4 流程(含健康度评分、10 必备字段、重构路线图),缺少轻量级「只描述现状、不评价优劣」的快速模式。 +- plan-grill 与 ios-engineer 协作时,需要明确委托契约:输出格式、纪律边界、与完整架构体检的区别。 + +## 变更类型 +- 新增能力 + +## 变更内容 +- 修改文件: + - `references/architecture_analysis.md`:新增「快速架构分析模式(plan-grill PG-005 委托用)」节,定义适用条件、输出格式、纪律约束。 +- 替代或合并旧规则:无替代;快速模式与完整 Phase 1-4 是互补关系,不存在重叠或冲突。 + +## 预期收益 +- plan-grill PG-005 委托 ios-engineer 时,有明确的轻量级输出契约,避免产出不必要的健康度评分、技术债等级或重构建议。 +- 快速模式输出只包含涉及文件、调用链、修改影响面、潜在风险 4 项,减少上下文浪费。 +- 纪律约束(不评价好坏、不输出优化建议、影响面必须具体到文件/方法)防止模糊结论。 + +## 验证 +- 结构校验:SKILL.md 未变更,references 变更为纯文档补充,不触发 rule_id 校验。 +- 场景回放:无新增场景;快速模式是 PG-005 的委托子流程,由 plan-grill 侧调用触发。 +- 残留风险:低。纯文档补充,不影响现有架构分析流程和规则逻辑。 + +## 状态 +- validated diff --git a/skills-engineering/ios-engineer/references/architecture_analysis.md b/skills-engineering/ios-engineer/references/architecture_analysis.md index f8539a2..beda159 100644 --- a/skills-engineering/ios-engineer/references/architecture_analysis.md +++ b/skills-engineering/ios-engineer/references/architecture_analysis.md @@ -181,6 +181,49 @@ - Phase 1 还没做就开始评分 - 用"建议加强测试""建议解耦"这类无位置、无证据的空洞结论 +## 快速架构分析模式(plan-grill PG-005 委托用) + +当 plan-grill 的 PG-005 委托 ios-engineer 做快速架构分析时,**跳过完整 Phase 1-4**,只产出以下内容。 + +### 适用条件 +- PG-003 盘问中涉及少量文件(通常 ≤10 个)的跨文件依赖关系。 +- 只需回答「调用链」「修改影响面」「模块耦合」,不需要健康度评分或重构路线图。 +- 与完整架构体检的区别:无 Phase 1-4 流程、无 10 必备字段、无健康度评分——只描述现状,不评价优劣。 + +### 输出格式 + +保存到 `.plan-reviews/<plan-slug>/architecture-analysis.md`: + +```markdown +# 架构分析 — <plan-slug> + +## 涉及文件 +- `<文件绝对路径>` — <一句话职责> +- ... + +## 调用链 +\``` +<入口类.方法>() + → <被调用类.方法>() // <触发条件或数据传递说明> + → ... +\``` + +## 修改影响面 +- 修改 `<文件A>`:会影响 `<文件B>`(<原因简述>)、`<文件C>`(<原因简述>) +- ... + +## 潜在风险(若有) +- <简洁描述,无代码证据则标注「待确认」> +- 若无风险写「本次分析未发现明显风险」 +``` + +### 纪律 +- 不输出健康度评分、技术债等级、重构路线图。 +- 不输出优化建议或「建议重构 XXX」类结论。 +- 只描述**现状**(调用关系 + 影响面),不评价好坏。 +- 调用链用文本箭头(`→`)表示,不需要 Mermaid 图;但如果调用链复杂度高(≥5 层或 ≥4 条分支),可加 Mermaid 辅助。 +- 修改影响面必须指出**具体影响的文件和方法**,不能写「影响多个模块」这类模糊表述。 + ## 与其他 ref 的协作 - 需要具体修法:按命中维度跳转到 [architecture_and_network.md](architecture_and_network.md) / [swift_concurrency.md](swift_concurrency.md) / [performance_optimization.md](performance_optimization.md) / [networking_patterns.md](networking_patterns.md) / [ui_state_patterns.md](ui_state_patterns.md) - 需要迁移风险门禁与阶段性回归:[migration_strategy.md](migration_strategy.md) diff --git a/skills-engineering/plan-grill/AGENT-BRIEF.md b/skills-engineering/plan-grill/AGENT-BRIEF.md new file mode 100644 index 0000000..8c6356e --- /dev/null +++ b/skills-engineering/plan-grill/AGENT-BRIEF.md @@ -0,0 +1,27 @@ +# plan-grill Agent 调用指南 + +## 一句话描述 + +一次一个问题盘问实现方案的决策树,每个问题给推荐答案,能查代码就查代码;跨文件依赖分析委托平台 engineer;确认前不执行;产出 PLAN.md 供 cross-model-review 接力。 + +## 何时调用 + +- **用户触发**:用户说 `【盘问】` / "grill me" / "锁定计划" / "盘问我的方案" +- **高风险任务前**:鉴权、schema、并发、迁移、支付等高风险构建前 +- **接力 problem-analysis**:problem-analysis 完成后,问题已清晰,需要锁定实现方案 + +## 关键行为 + +1. 阅读 `SKILL.md` + `references/plan_grill.md` 全文。 +2. 一次一个问题(PG-001),每问给推荐答案 + 理由(PG-002)。 +3. 能查代码回答的,直接查,不问用户(PG-003)。 +4. PG-003 涉及跨文件/跨模块依赖分析且已加载平台 engineer 时,暂停盘问,委托快速架构分析并把 `architecture-analysis.md` 路径写回 PLAN.md(PG-005)。 +5. 决策树解析完且用户确认后,写 PLAN.md(PG-004),七段填实(Goal / Constraints & assumptions / Approach / Key decisions & tradeoffs / Validation plan / Risks / Out of scope)。 +6. 确认前不执行计划。 + +## 不调用的情况 + +- trivial 改动(typo、格式化、单点语法) +- 纯执行任务(已知明确指令) +- 用户明确「直接做」「不要盘问」 +- problem-analysis 未完成(问题本身未审查) diff --git a/skills-engineering/plan-grill/OUT-OF-SCOPE.md b/skills-engineering/plan-grill/OUT-OF-SCOPE.md new file mode 100644 index 0000000..cafe0bc --- /dev/null +++ b/skills-engineering/plan-grill/OUT-OF-SCOPE.md @@ -0,0 +1,14 @@ +# plan-grill 范围外 + +本 skill 负责**实现方案的盘问与锁定**,不负责问题审查、代码审查、跨模型对抗。 + +## 不处理的内容 + +- **问题本身审查**:问题是否含逻辑错误、矛盾前提、真实需求拆解,由 `problem-analysis`(PA-001/002/003)负责。plan-grill 在 problem-analysis 完成后启动。 +- **跨模型对抗审查**:PLAN.md 锁定后由已选 reviewer 对抗审查,是 `cross-model-review` 的职责。plan-grill 只产出 PLAN.md,不调用 reviewer。 +- **已写代码的审查**:审查已实现的代码由 `ios-engineer/references/review_checklists.md` 或 `cross-model-review`(审查计划而非代码)处理。 +- **执行计划**:plan-grill 只锁定计划,不执行。执行由后续对话或 ios-engineer skill 承接。 + +## 触发门控 + +仅在 problem-analysis 完成后触发。若问题本身未审查,先加载 problem-analysis。 diff --git a/skills-engineering/plan-grill/SKILL.md b/skills-engineering/plan-grill/SKILL.md new file mode 100644 index 0000000..ec0c187 --- /dev/null +++ b/skills-engineering/plan-grill/SKILL.md @@ -0,0 +1,38 @@ +--- +name: plan-grill +description: 需求对齐/盘问锁定计划——一次一个问题盘问决策树,每个问题给推荐答案,能查代码就查代码,确认前不执行。产出 PLAN.md 供后续 cross-model-review 接力。基于 Matt Pocock 的 grill-me(MIT)。 +--- + +# Plan Grill + +## 强制入口 + +命中本 skill 时,**必须先完整阅读** [references/plan_grill.md](references/plan_grill.md) 并按其中条款执行。 + +- 不得以 preamble、Cursor 规则摘要或其它二次摘要代替该文件全文。 +- 本 skill 是 `cross-model-review` 的 Act 1;若需要跨模型对抗审查,盘问锁定后接力 `cross-model-review`。 + +## 五条核心规则 + +- [PG-001] **逐一提问**:一次只问一个问题,等用户回答后再继续。禁止一次抛出多个问题。 +- [PG-002] **给推荐答案**:每个问题须给出推荐答案 + 一句理由,让用户可以快速确认或反驳,而非从零思考。 +- [PG-003] **遍历设计树**:沿决策树分支逐一解决依赖;能通过探索代码库回答的问题,直接查代码,不问用户。 +- [PG-004] **锁定产出**:决策树解析完且与用户达成共识后,产出 `PLAN.md`(Goal / Constraints & assumptions / Approach / Key decisions & tradeoffs / Validation plan / Risks / Out of scope)。**确认前不执行计划。** +- [PG-005] **架构分析委托**:PG-003 探索代码库时,若涉及跨文件/跨模块依赖分析,且已加载平台 engineer skill(如 `ios-engineer`),则暂停盘问,读取涉及文件,按平台 engineer 的「快速架构分析」模式产出到 `.plan-reviews/<plan-slug>/architecture-analysis.md`,并在后续 PLAN.md 中写入该相对路径,然后继续盘问。若未加载平台 engineer,则在 PLAN.md 中用文字描述依赖关系。plan-grill 自身不分析任何语言/框架的架构。 + +细则见 [references/plan_grill.md](references/plan_grill.md)。 + +## 何时加载 + +- **默认触发**:用户说 `【盘问】` / `/plan-grill` / `/grill-me` / "grill me" / "锁定计划" / "盘问我的方案" / "盘我" / "拷问方案" / "先锁计划" / "先别写代码" / "stress-test the plan" / "requirements interview"。 +- **建议触发**(不自动):高风险任务(鉴权、schema、并发、迁移、支付)前,可主动建议用户触发,但需用户确认;不得与用户明确"直接做"的工作流冲突。 +- **跳过**:trivial 改动(typo、格式化、单点语法)、纯执行任务、用户明确"直接做"。 + +## 与相邻 skill 的分工 + +| Skill | 分工 | +|-------|------| +| `problem-analysis`(PA-001/002/003) | 分析**问题本身**的合理性与真实需求 | +| **plan-grill(本 skill)** | 问题清晰后,盘问**实现方案**的决策树并锁定计划 | +| `cross-model-review` | plan-grill 锁定后,跨模型对抗审查 PLAN.md | +| `engineering-discipline`(GR-002) | 问题**描述不清**时前置确认 | diff --git a/skills-engineering/plan-grill/examples/plan-example-login-rate-limit.md b/skills-engineering/plan-grill/examples/plan-example-login-rate-limit.md new file mode 100644 index 0000000..7c54e1d --- /dev/null +++ b/skills-engineering/plan-grill/examples/plan-example-login-rate-limit.md @@ -0,0 +1,94 @@ +# 示例:plan-grill 盘问产出 → 用户登录速率限制 PLAN.md + +> 本文件是 plan-grill skill 的例样,供人类和 Agent 参考 PLAN.md 七段格式与填写标准。 +> 不参与 Agent 运行时加载(sync-skills.sh 白名单不同步 examples/)。 +> +> 此例与 `cross-model-review/examples/regression-login-rate-limit.md` 形成对照: +> - **本文件**:plan-grill 盘问**修正后最终锁定**的 PLAN.md(已修正 INCR+EXPIRE 原子性等缺陷) +> - **cross-model-review example**:展示盘问前的初版 PLAN.md 含哪些缺陷,以及跨模型审查如何发现它们 + +## 场景 + +用户提出「给登录接口加速率限制,防暴力破解」。plan-grill 逐一盘问实现方案的决策树,与用户达成共识后锁定以下 PLAN.md。 + +## 盘问决策树(已解析) + +| 决策点 | 提问 | 用户选择 | 理由 | +|--------|------|----------|------| +| 存储后端 | Redis 还是数据库? | Redis | 低延迟、原生 TTL、INCR 原子性 | +| Key 粒度 | 组合 key 还是分开 key? | 组合 key `u:{username}:ip:{ip}` | 避免仅 per-username 导致的用户 DoS | +| 原子性 | 如何保证 INCR+EXPIRE? | Lua 脚本 | 避免 INCR 后崩溃导致 key 永不过期 | +| 窗口类型 | 固定窗口还是滑动窗口? | 固定窗口 | 防暴破不需要精确计量,实现简单 | +| 阈值/锁定时长 | 几次失败锁定多久? | 5 次 / 15 分钟 | OWASP 建议下限 | +| IP 来源 | 如何获取真实 IP? | 受信代理链提取 | 避免 X-Forwarded-For 伪造 | +| 成功处理 | 成功登录是否重置? | 是,DEL 计数器 | 避免跨会话累积 | +| 锁定响应 | 返回什么给客户端? | Retry-After = PTTL | 动态剩余时间,非硬编码 | +| Redis 故障 | 降级策略? | fail-open + 告警 | 避免 Redis 故障导致全员无法登录 | +| Username 规范化 | key 前是否需要? | 小写规范化 | 避免大小写变体绕过 | + +--- + +## PLAN.md(盘问锁定版) + +> 以下为 plan-grill 按 PG-004 产出的标准七段 PLAN.md。 + +```markdown +# Plan: 用户登录速率限制(防暴力破解) + +## Goal +防止暴力破解登录,限制每个用户/IP 配对的登录失败次数,超限后临时锁定该配对。 + +## Constraints & assumptions +- Web 服务多实例部署,不能依赖单实例进程内存 +- Redis 可用作共享低延迟存储 +- 应用部署在受信反向代理之后,可从可信 header 提取规范客户端 IP +- Username 大小写不敏感,登录前可规范化 +- 本版本接受短固定窗口近似,精确滑动窗口暂不在范围内 + +## Approach +规范化提交的 username 后构造 Redis key。仅从受信代理链提取客户端 IP。每次登录失败时,通过 Redis Lua 脚本对 `loginfail:v1:u:{hash(username)}:ip:{ip}` 原子递增失败计数并设置 TTL(首次 15 分钟)。配对在 15 分钟内超过 5 次失败后,设置同配对的锁定 key(15 分钟 TTL)并清除计数器 key。锁定期间在密码验证前拒绝请求,返回 `Retry-After`(基于锁定 key 剩余 PTTL)。登录成功时删除该配对的计数器 key。锁定触发时发送结构化安全日志和 metrics。 + +## Key decisions & tradeoffs +- 配对粒度 key(`u:{username}:ip:{ip}`)而非仅 per-username:降低针对性账号锁定 DoS 风险;对高度分布式攻击较弱,后续可叠加 CAPTCHA 或自适应风险评分 +- Lua 脚本而非分开 INCR + EXPIRE:保持计数更新和 TTL 设置原子性;增加 Redis 脚本依赖但避免计数器永不过期 +- 固定窗口而非滑动窗口:实现简单,满足初始暴破控制需求;边界突发可接受 +- 对 Redis key 和日志中 username 做哈希:避免在基础设施 key 中存储原始标识符;略微增加手动调试复杂度 +- Redis 故障时 fail-open + 告警:保持登录可用性;临时削弱暴破防护,必须可观测 + +## Validation plan +- 单元测试:username 规范化、受信客户端 IP 提取、key 构造、基于剩余 PTTL 的 Retry-After +- Redis 集成测试:Lua 脚本首次失败设 TTL、并发失败不丢计数、脚本路径后无 key 缺 TTL +- 行为测试:第 1-5 次到达密码验证、第 6 次同配对被拒、不同 IP 不自动锁定、登录成功清除配对计数器 +- 过期测试:锁定 TTL 自动过期、清除的计数器不立即重新锁定、Retry-After 随时间递减 +- 可观测性检查:锁定事件输出结构化日志并递增 metrics(不含原始 username) + +## Risks / non-blocking open questions +- 跨多 IP 的分布式低慢攻击可能绕过配对粒度限制;后续可叠加 username 级 CAPTCHA 或自适应风险评分 +- NAT 密集型网络下同 username/IP 配对可能遇到共享 IP 摩擦;配对粒度比纯 IP 限制减少了影响范围 +- Redis fail-open 在故障期间削弱保护;必须告警以便运维响应 + +## Out of scope +- CAPTCHA、MFA 升级、基于风险的认证策略 +- 全局 IP 信誉或分布式滥用情报源 +- 账户恢复、帮助台解锁流程、管理手动覆盖工具 +``` + +## 示例使用说明 + +### Agent 如何参考本例 + +执行 plan-grill 时: +1. 盘问结束后,对照本例检查 PLAN.md 是否七段完整 +2. 每段内容是否具体(无占位符如「TODO」「待定」) +3. Key decisions 是否有明确的「选 A 而非 B」对比 +4. Validation plan 是否包含可执行的测试路径而非含糊描述 + +### 常见缺失(检查清单) + +- [ ] Goal 是否一句话说清「解决什么」,而非「做什么功能」 +- [ ] Constraints 是否区分了硬约束和未验证假设 +- [ ] Approach 是否足够具体让另一个工程师能实施 +- [ ] Key decisions 是否有 tradeoff 说明(选什么、不选什么、为什么) +- [ ] Validation plan 是否可执行(不是「充分测试」这类空话) +- [ ] Risks 是否标注了 non-blocking(明确什么已知但不阻塞实施) +- [ ] Out of scope 是否显式列出不做的事(防止范围蔓延) diff --git a/skills-engineering/plan-grill/references/plan_grill.md b/skills-engineering/plan-grill/references/plan_grill.md new file mode 100644 index 0000000..c0dc42e --- /dev/null +++ b/skills-engineering/plan-grill/references/plan_grill.md @@ -0,0 +1,159 @@ +<!-- last-verified: 2026-07 --> +# 计划盘问(Plan Grill) + +> **真值来源**:本文件为唯一详规正文。`plan-grill/SKILL.md` 为入口;各端完整副本由 `scripts/sync-skills.sh` 同步到 `~/.codex/skills/`、`~/.claude/skills/`、`~/.cursor/skills/`、`~/.gemini/skills/`;Cursor 项目内另由 `sync-agent-preamble.sh` 生成 `.cursor/rules/plan-grill.mdc`。 + +## 定位 + +plan-grill 解决 AI 辅助编码的第 1 类失败模式:**你和 AI 对"构建什么"未达成共识**。通过一次一个问题的盘问,把模糊需求逼成可执行的锁定计划。 + +本 skill 基于 Matt Pocock 的 `grill-me`(MIT 许可),适配本项目结构化 skill 框架。 + +## 与 problem-analysis 的衔接 + +| 阶段 | skill | 做什么 | +|------|-------|--------| +| 1. 问题审查 | `problem-analysis` | 检查问题本身是否含逻辑错误、矛盾前提;拆解真实需求 | +| 2. 方案盘问 | **plan-grill** | 问题清晰后,盘问实现方案的决策树,逐一锁定 | +| 3. 跨模型审查(可选) | `cross-model-review` | 锁定后,已选 reviewer 对抗审查 PLAN.md | + +problem-analysis 未完成时,plan-grill 不开始——否则会在错误前提上盘问。 + +## 盘问规则(PG-001 ~ PG-005 详规) + +### PG-001 逐一提问 + +- **一次一个问题**。问完即停,等用户回答。 +- 禁止用「另外还有…」「顺便问下…」追加第二问。 +- 若问题有依赖,先问被依赖的那个;依赖未明时不下钻。 +- 一次抛多个问题会让用户 bewildered(Matt Pocock 原话),违反本规则。 + +### PG-002 给推荐答案 + +每个问题须包含: + +1. **问题本身**(一句话,具体到决策点) +2. **推荐答案**(一句话,给方向而非含糊「看情况」) +3. **理由**(一句,为什么推荐这个) + +格式: + +``` +Q: <问题> +推荐: <答案> +理由: <一句> +``` + +让用户可以「确认 / 反驳 / 跳过」,而非从零思考。推荐答案不是替用户决定,是降低决策成本。 + +### PG-003 遍历设计树 + +- 把方案拆成决策树,按依赖顺序逐一解决。 +- **能查代码就查代码**:如果一个问题可以通过探索代码库回答(如「这个函数返回什么类型」「现有 schema 有没有 X 字段」「这个配置项的默认值」),直接查,不问用户。 +- 用户回答后,沿其分支下钻下一层;不横向跳跃。 +- 决策树完全解析(无未决分支)后才进入产出。 + +### PG-004 锁定产出 + +决策树解析完且与用户达成共识后,写入 `PLAN.md`: + +```markdown +# Plan: <一句话标题> + +## Goal +<要解决什么,一句话> + +## Constraints & assumptions +- <约束 1:必须满足的硬条件> +- <假设 1:未验证但当前假定为真> + +## Approach +<怎么做,2-5 句> + +## Key decisions & tradeoffs +- <决策 1>:选 A 而非 B,因为… +- <决策 2>:… + +## Validation plan +- <如何证明方案有效:测试/验收路径> + +## Risks / non-blocking open questions +- <风险 1:non-blocking,可保留> +- <或显式 "None"> + +## Out of scope +- <明确不做的事> +``` + +写入后告知用户:「PLAN.md 已锁定。如需跨模型对抗审查,接力 `cross-model-review`。」 + +### PG-005 架构分析委托 + +PG-003 探索代码库时,若涉及**跨文件/跨模块依赖分析**(如追踪类的调用链、理解模块间耦合关系、评估修改影响面),plan-grill 不亲自产出架构分析——因为 plan-grill 是平台无关的,不具备任何语言或框架的架构知识。 + +**触发条件**: +- PG-003 探索代码时,分析涉及多个源文件之间的依赖关系 +- 盘问中涉及「这个类的依赖是什么」「改了 A 会影响哪些模块」「调用链怎么走」等跨文件问题 + +**执行方式(已加载平台 engineer 时)**: + +1. **暂停盘问**:告知用户——「PG-003 发现跨文件依赖,需要 _[平台 engineer 名称]_ 做快速架构分析,稍后继续盘问。」 +2. **定位源文件**:列出 PG-003 当前探索中涉及的所有源文件路径(绝对路径)。 +3. **读取并分析**:逐个读取这些源文件,按平台 engineer 的「快速架构分析」模式输出。注意:不是完整架构体检,不输出健康度评分、技术债等级、重构路线图——只描述调用关系 + 修改影响面。 +4. **保存文档**:写入 `.plan-reviews/<plan-slug>/architecture-analysis.md`;`<plan-slug>` 必须与后续 PLAN / cross-model-review 归档目录一致,若尚未锁定 slug,先使用本轮计划的临时 slug,并在 PLAN 中保留该相对路径。 +5. **写回计划上下文**:后续 PG-004 产出 PLAN.md 时,必须在 Constraints & assumptions 或 Risks 段写入 `Architecture analysis: .plan-reviews/<plan-slug>/architecture-analysis.md`,确保 cross-model-review reviewer 能按 PLAN.md 找到该文件。 +6. **回到盘问**:告知用户分析完成,继续 PG-003 盘问。`architecture-analysis.md` 中发现的潜在风险可升格为盘问决策点。 + +**执行方式(未加载平台 engineer 时)**: + +- 在 PLAN.md 的 Approach 或 Risks 段中用文字描述关键依赖关系。 +- 不单独产出 architecture-analysis.md。 +- 不做任何语言/框架层面的架构推断。 + +**注意事项**: +- plan-grill 自身不分析任何语言/框架的架构,只做盘问和计划锁定。 +- 架构分析是平台 engineer 的职责,每个平台有自己特有的模块划分、分层方式和关注维度。 +- 产出的 architecture-analysis.md 必须通过 PLAN.md 明确引用;cross-model-review 只以 PLAN.md 及其引用文件作为稳定入口。 + +## 何时停止盘问 + +满足以下全部条件才停: + +1. 决策树无未决分支(每个叶子节点都有明确选择) +2. 用户对每个决策确认或接受推荐 +3. PLAN.md 七段(Goal / Constraints & assumptions / Approach / Key decisions / Validation plan / Risks / Out of scope)都能填实 +4. **blocking open questions 必须为空**:未决的阻塞性问题必须在盘问阶段解决,不得遗留。 +5. **non-blocking risks 可保留**:已知但不阻塞实施的风险,写入 Risks 段即可,不必在盘问阶段消除。 + +任一不满足,继续问下一个未决点。 + +## 跳过条件 + +- trivial 改动(typo、格式化、单点语法、直接翻译) +- 用户明确「直接做」「不要盘问」 +- 纯执行任务(已知明确指令,只需执行) + +## 盘问质量自检 + +盘问结束前过一遍: + +- [ ] 是否每个问题都给了推荐答案 + 理由? +- [ ] 是否有本可查代码却问了用户的问题?(应改为查代码) +- [ ] 决策树是否还有未决叶子? +- [ ] PLAN.md 七段是否都填实,无占位符? +- [ ] blocking open questions 是否已清空?non-blocking risks 是否已记录? +- [ ] 涉及跨文件依赖分析时,是否已委托平台 engineer 产出 architecture-analysis.md?(PG-005) + +## 与 cross-model-review 的接力 + +plan-grill 产出的 `PLAN.md` 是 `cross-model-review` 的输入。若用户在盘问后说「让另一个模型审查」「cross review」「对抗审查」,则: + +1. plan-grill 完成(PLAN.md 已写) +2. 加载 `cross-model-review` skill +3. cross-model-review 读取 PLAN.md,自动发现可用 CLI(codex/gemini/claude),推荐组合并让用户选择,调用已选 reviewer 对抗审查 + +详见 `cross-model-review/references/cross_model_review.md`。 + +## 致谢 + +本 skill 基于 Matt Pocock 的 `grill-me`(MIT 许可,https://github.com/mattpocock/skills);盘问规则源自其 `grilling` 实现。适配本项目结构化 skill 框架。 diff --git a/skills-engineering/plan-reviews/.gitignore b/skills-engineering/plan-reviews/.gitignore new file mode 100644 index 0000000..aa450b0 --- /dev/null +++ b/skills-engineering/plan-reviews/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tmp +plan-reviews-kb.db diff --git a/skills-engineering/plan-reviews/README.md b/skills-engineering/plan-reviews/README.md new file mode 100644 index 0000000..745d356 --- /dev/null +++ b/skills-engineering/plan-reviews/README.md @@ -0,0 +1,129 @@ +# plan-reviews knowledge base + +> 本地嵌入式知识库 —— 将 `.plan-reviews/` 目录下的 plan-grill 和 cross-model-review 产物自动索引,提供语义搜索和实体图谱,**零外部服务依赖、无需数据库**。 + +核心思路:`.plan-reviews` 存储的是高度结构化的 PLAN.md 文件,数据量小(几十到几百个 plan),通过内存余弦相似度 + 单 JSON 缓存文件即可实现全文搜索和知识图谱,完全不需要重型基础设施。 + +## 快速开始 + +```bash +cd skills-engineering/plan-reviews +npm install +npm run sync # 同步 .plan-reviews/ 到本地知识库 +``` + +## 使用方式 + +### 1. 作为库(在 skill 中调用) + +```typescript +import { PlanReviewsKB } from "@i-stack/plan-reviews-kb"; + +const kb = await PlanReviewsKB.init({ projectRoot: process.cwd() }); + +// 同步所有 plan +await kb.sync(); + +// 搜索历史 plan +const results = await kb.search({ + query: "Redis rate limiting atomic operations", + limit: 5, +}); + +// 格式化输出 +console.log(kb.formatResults(results)); + +console.log(kb.stats); +// { plans: 1, entities: 15, relations: 22, chunks: 8 } + +kb.close(); +``` + +### 2. 搜索指定类型的实体 + +```typescript +const results = await kb.search({ + query: "CAPTCHA", + entityType: "technology", // 只搜索技术实体 +}); +``` + +### 3. 搜索特定 plan + +```typescript +const results = await kb.search({ + query: "atomic Lua script", + planId: "2026-07-06-login-rate-limit", // 限定范围 +}); +``` + +## 架构 + +``` +.plan-reviews/ skills-engineering/plan-reviews/ +├── 2026-07-06-xxx/ src/ +│ ├── PLAN.md ──sync──→ ├── parser.ts 结构化解析 PLAN.md +│ ├── PLAN-REVIEW-LOG.md ──→ ├── extractor.ts 实体/关系提取(无 LLM) +│ ├── architecture-analysis.md ─→│ PG-005 架构分析搜索 chunk +│ └── SUMMARY.md ──→ ├── store.ts JSON 缓存持久化 +│ ├── embed.ts Embedding API +│ ├── vector.ts 内存向量索引(余弦相似度) +│ ├── search.ts 组合搜索(语义+图谱) +│ ├── sync.ts 增量同步引擎 +│ └── index.ts 主入口 +│ +└── .kb-index.json ←── JSON 缓存文件(自动生成) +``` + +## 数据流 + +``` +1. scanPlansDir() 扫描 .plan-reviews/ 目录 + ↓ +2. parsePlan() 解析 PLAN.md 七段结构 + ↓ +3. extractFromArtifact() 提取实体(goal/decision/risk/technology...) + 关系(uses/addresses/constrains...) + ↓ +4. planToChunks() 切分文本段落 + ↓ +5. embedBatch() 生成 Embedding 向量 + ↓ +6. upsertPlan/Entities/Chunks 写入 JSON 缓存 + 内存向量索引 + ↓ +7. search() 语义相似度 + 图谱遍历 → 结果 +``` + +## 知识图谱实体类型 + +| Entity Type | 来源 | 示例 | +|---|---|---| +| `goal` | PLAN.md → Goal | "Reduce credential-stuffing risk" | +| `constraint` | PLAN.md → Constraints | "Deployed behind trusted reverse proxy" | +| `decision` | PLAN.md → Key Decisions | "Pair-scoped key instead of username-only" | +| `risk` | PLAN.md → Risks | "Distributed low-and-slow attacks" | +| `technology` | Approach/Decisions 文本 | "TypeScript", "Docker" | +| `service` | Approach/Decisions 文本 | "Redis", "PostgreSQL" | +| `out_of_scope` | PLAN.md → Out of Scope | "CAPTCHA, MFA step-up" | + +## 关系类型 + +| Relation | 含义 | +|---|---| +| `uses` | Decision 使用某个 Technology/Service | +| `addresses` | Decision 解决某个 Goal;Risk 威胁某个 Goal | +| `constrains` | Constraint 限制某个 Decision | +| `mitigates` | Decision 缓解某个 Risk | + +## 环境变量 + +| 变量 | 说明 | 默认值 | +|---|---|---| +| `EMBEDDING_API_KEY` | Embedding API 密钥 | (空,跳过语义搜索) | +| `EMBEDDING_BASE_URL` | Embedding API 地址 | `https://api.openai.com/v1` | +| `EMBEDDING_MODEL` | Embedding 模型 | `bge-m3` | +| `VECTOR_SIZE` | 向量维度 | 根据模型自动推断 | + +> **配置方式**:在 `env/secrets.json` 中添加 `embedding` 字段,填入你的 `key` 和 `url`,即可自动映射为 `EMBEDDING_API_KEY` 和 `EMBEDDING_BASE_URL`。 + +**无 Embedding API 也能用**:即使不配置 Embedding API,图谱搜索(实体/关系匹配)仍然完全可用。语义搜索会自动降级跳过。 diff --git a/skills-engineering/plan-reviews/package-lock.json b/skills-engineering/plan-reviews/package-lock.json new file mode 100644 index 0000000..32f6749 --- /dev/null +++ b/skills-engineering/plan-reviews/package-lock.json @@ -0,0 +1,1831 @@ +{ + "name": "@i-stack/plan-reviews-kb", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@i-stack/plan-reviews-kb", + "version": "0.1.0", + "devDependencies": { + "@types/node": "^22.13.0", + "tsx": "^4.19.3", + "typescript": "^5.7.3", + "vitest": "^4.1.8" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "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/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "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/@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/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.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/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.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/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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/es-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz", + "integrity": "sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==", + "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/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/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/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "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/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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "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": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "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/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "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-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "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", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "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/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "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" + } + } + } +} diff --git a/skills-engineering/plan-reviews/package.json b/skills-engineering/plan-reviews/package.json new file mode 100644 index 0000000..c788b97 --- /dev/null +++ b/skills-engineering/plan-reviews/package.json @@ -0,0 +1,34 @@ +{ + "name": "@i-stack/plan-reviews-kb", + "version": "0.1.0", + "description": "Local knowledge base for .plan-reviews — embeds semantic search, entity graph, and structured plan parsing without external services. Pure JSON cache, zero native dependencies.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "dev": "tsx watch src/cli.ts", + "build": "tsc", + "typecheck": "tsc --noEmit", + "cli": "tsx src/cli.ts", + "sync": "tsx src/cli.ts sync", + "search": "tsx src/cli.ts search", + "reset": "tsx src/cli.ts reset", + "stats": "tsx src/cli.ts stats", + "visualize": "tsx src/cli.ts visualize", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": {}, + "devDependencies": { + "@types/node": "^22.13.0", + "tsx": "^4.19.3", + "typescript": "^5.7.3", + "vitest": "^4.1.8" + } +} diff --git a/skills-engineering/plan-reviews/src/cli.ts b/skills-engineering/plan-reviews/src/cli.ts new file mode 100644 index 0000000..fc3793a --- /dev/null +++ b/skills-engineering/plan-reviews/src/cli.ts @@ -0,0 +1,109 @@ +#!/usr/bin/env node +/** + * CLI for plan-reviews knowledge base. + * + * Usage: + * npx tsx src/cli.ts sync # Sync .plan-reviews/ to KB + * npx tsx src/cli.ts search "query" # Search the KB + * npx tsx src/cli.ts stats # Show KB statistics + * npx tsx src/cli.ts reset # Full reset and re-sync + */ + +import { PlanReviewsKB } from "./index.js"; +import { generateKnowledgeGraph } from "./visualize.js"; + +const args = process.argv.slice(2); +const command = args[0] ?? "help"; +const query = args.slice(1).join(" ") || args[0] || ""; + +// Parse --output flag +function parseOutputFlag(): string | undefined { + const idx = args.indexOf("--output"); + return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined; +} + + + +async function main() { + const kb = await PlanReviewsKB.init(); + + switch (command) { + case "sync": { + console.log("Syncing .plan-reviews/ to knowledge base..."); + const stats = await kb.sync(); + console.log( + `Done: ${stats.added} added, ${stats.modified} modified, ` + + `${stats.removed} removed, ${stats.skipped} skipped`, + ); + if (stats.errors.length > 0) { + console.log(`\nErrors (${stats.errors.length}):`); + for (const err of stats.errors) console.log(` - ${err}`); + } + break; + } + + case "search": { + if (!query || query === "search") { + console.log("Usage: cli.ts search <query>"); + break; + } + console.log(`Searching: "${query}"\n`); + const results = await kb.search({ query }); + console.log(kb.formatResults(results)); + break; + } + + case "stats": { + const s = kb.stats; + console.log("Knowledge Base Statistics:"); + console.log(` Plans: ${s.plans}`); + console.log(` Entities: ${s.entities}`); + console.log(` Relations: ${s.relations}`); + console.log(` Chunks: ${s.chunks}`); + break; + } + + case "reset": { + console.log("Resetting knowledge base..."); + const stats = await kb.reset(); + console.log( + `Done: ${stats.added} added, ${stats.modified} modified, ` + + `${stats.removed} removed, ${stats.skipped} skipped`, + ); + break; + } + + case "visualize": { + const output = parseOutputFlag(); + const outputPath = generateKnowledgeGraph({ output }); + console.log(`Knowledge graph generated: ${outputPath}`); + console.log("Open it in your browser to explore."); + break; + } + + default: + console.log("plan-reviews knowledge base CLI"); + console.log(""); + console.log("Commands:"); + console.log(" sync Sync .plan-reviews/ to knowledge base"); + console.log(" search <query> Search the knowledge base"); + console.log(" stats Show KB statistics"); + console.log(" reset Full reset and re-sync"); + console.log(" visualize Generate interactive knowledge graph HTML"); + console.log(" --output <path> Custom output path (default: .plan-reviews/knowledge-graph.html)"); + + console.log(""); + console.log("Environment:"); + console.log(" EMBEDDING_API_KEY Embedding API key (optional)"); + console.log(" EMBEDDING_BASE_URL Embedding API base URL"); + console.log(" EMBEDDING_MODEL Embedding model name"); + break; + } + + kb.close(); +} + +main().catch((err) => { + console.error("Error:", err.message); + process.exit(1); +}); diff --git a/skills-engineering/plan-reviews/src/config.ts b/skills-engineering/plan-reviews/src/config.ts new file mode 100644 index 0000000..d6fb366 --- /dev/null +++ b/skills-engineering/plan-reviews/src/config.ts @@ -0,0 +1,118 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +/** + * Configuration for the plan-reviews knowledge base. + * + * Sources (in priority order): + * 1. Programmatic overrides + * 2. Environment variables (EMBEDDING_API_KEY, EMBEDDING_BASE_URL) + * 3. env/secrets.json → embedding.key / embedding.url + * 4. Defaults + * + * The only external dependency is the Embedding API. + */ + +export interface PlanReviewsConfig { + /** Root directory containing .plan-reviews/ (defaults to auto-detected) */ + projectRoot: string; + /** Path to the JSON cache file (.plan-reviews/.kb-index.json) */ + indexPath: string; + /** Embedding API key */ + embeddingApiKey: string; + /** Embedding API base URL (OpenAI-compatible) */ + embeddingBaseUrl: string; + /** Embedding model name */ + embeddingModel: string; + /** Vector dimension (must match embedding model) */ + vectorSize: number; + /** Whether semantic search is enabled (requires embeddingApiKey) */ + semanticEnabled: boolean; +} + +const DEFAULT_VECTOR_SIZES: Record<string, number> = { + "bge-m3": 1024, + "text-embedding-ada-002": 1536, + "text-embedding-3-large": 3072, + "text-embedding-3-small": 1536, +}; + +function optionalEnv(key: string, fallback: string): string { + return process.env[key] ?? fallback; +} + +interface EmbeddingSecrets { + key: string; + url: string; +} + +/** + * Read the `embedding` field from env/secrets.json. + * Falls back to empty values if the file or field is missing. + */ +function loadEmbeddingSecrets(projectRoot: string): EmbeddingSecrets { + try { + const secretsPath = resolve(projectRoot, "env", "secrets.json"); + if (!existsSync(secretsPath)) return { key: "", url: "" }; + const raw = readFileSync(secretsPath, "utf-8"); + const secrets = JSON.parse(raw); + const embedding = secrets?.embedding; + if (!embedding || typeof embedding !== "object") return { key: "", url: "" }; + return { + key: typeof embedding.key === "string" ? embedding.key : "", + url: typeof embedding.url === "string" ? embedding.url : "", + }; + } catch { + return { key: "", url: "" }; + } +} + +export function loadConfig(overrides?: Partial<PlanReviewsConfig>): PlanReviewsConfig { + const projectRoot = overrides?.projectRoot ?? discoverProjectRoot(); + const embeddingModel = overrides?.embeddingModel + ?? optionalEnv("EMBEDDING_MODEL", "bge-m3"); + const rawVectorSize = process.env.VECTOR_SIZE ?? ""; + const vectorSize = rawVectorSize + ? parseInt(rawVectorSize, 10) + : (DEFAULT_VECTOR_SIZES[embeddingModel] ?? 1024); + + const secrets = loadEmbeddingSecrets(projectRoot); + const embeddingApiKey = overrides?.embeddingApiKey + ?? (optionalEnv("EMBEDDING_API_KEY", "") || secrets.key); + const embeddingBaseUrl = overrides?.embeddingBaseUrl + ?? (optionalEnv("EMBEDDING_BASE_URL", "") || secrets.url || "https://api.openai.com/v1"); + + return { + projectRoot, + indexPath: overrides?.indexPath + ?? `${projectRoot}/.plan-reviews/.kb-index.json`, + embeddingApiKey, + embeddingBaseUrl, + embeddingModel, + vectorSize, + semanticEnabled: embeddingApiKey.length > 0, + }; +} + +/** + * Walk upward from cwd to find the project root by locating + * the .plan-reviews/ or .codebuddy/ directory. + */ +function discoverProjectRoot(): string { + let dir = process.cwd(); + + for (let i = 0; i < 10; i++) { + if ( + existsSync(`${dir}/.plan-reviews`) + || existsSync(`${dir}/.codebuddy`) + ) { + return dir; + } + const parent = dir.substring(0, dir.lastIndexOf("/")); + if (parent === dir || parent === "") break; + dir = parent; + } + + // Fallback to cwd + return process.cwd(); +} diff --git a/skills-engineering/plan-reviews/src/embed.ts b/skills-engineering/plan-reviews/src/embed.ts new file mode 100644 index 0000000..8fb8595 --- /dev/null +++ b/skills-engineering/plan-reviews/src/embed.ts @@ -0,0 +1,79 @@ +/** + * Embedding API client for plan-reviews knowledge base. + * + * Directly ported from rag-gateway's src/vector/embedding.ts, + * using the same OpenAI-compatible API pattern. + * + * Uses native fetch (no OpenAI SDK) to avoid the dimension misreporting + * issues observed with bge-m3. + */ + +import type { PlanReviewsConfig } from "./config.js"; + +export class EmbeddingService { + private apiKey: string; + private baseUrl: string; + private model: string; + + constructor(config: PlanReviewsConfig) { + this.apiKey = config.embeddingApiKey; + this.baseUrl = config.embeddingBaseUrl.replace(/\/+$/, ""); + this.model = config.embeddingModel; + } + + /** + * Whether this service is usable (API key is configured). + */ + get isAvailable(): boolean { + return this.apiKey.length > 0; + } + + /** + * Generate an embedding vector for a single text string. + */ + async embed(text: string): Promise<number[]> { + const results = await this._create(text); + return results[0]; + } + + /** + * Generate embedding vectors for multiple texts (batched). + * Sends all texts in a single API call for efficiency. + */ + async embedBatch(texts: string[]): Promise<number[][]> { + if (texts.length === 0) return []; + return this._create(texts); + } + + /** + * Internal: call the OpenAI-compatible embeddings API via native fetch. + */ + private async _create(input: string | string[]): Promise<number[][]> { + const url = `${this.baseUrl}/embeddings`; + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}`, + }, + body: JSON.stringify({ + model: this.model, + input, + }), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Embedding API ${response.status}: ${text}`); + } + + const json = (await response.json()) as { + data: Array<{ embedding: number[]; index: number }>; + }; + + // Sort by index to maintain order + return json.data + .sort((a, b) => a.index - b.index) + .map((d) => d.embedding); + } +} diff --git a/skills-engineering/plan-reviews/src/extractor.ts b/skills-engineering/plan-reviews/src/extractor.ts new file mode 100644 index 0000000..630b6d6 --- /dev/null +++ b/skills-engineering/plan-reviews/src/extractor.ts @@ -0,0 +1,386 @@ +/** + * Entity & Relationship extractor for plan artifacts. + * + * Unlike rag-gateway's EntityExtractor (which calls an LLM with a + * structured prompt), this extractor works on the pre-parsed + * PlanSections from parser.ts. It applies heuristics to extract: + * + * - Technologies/Services from Approach and Decisions sections + * - Decision entities from the Decisions section + * - Risk entities from the Risks section + * - Constraint entities from the Constraints section + * - Goal, OutOfScope entities + * - Flaw entities from PLAN-REVIEW-LOG.md + * + * Relationships are constructed between these entities based on + * section co-occurrence and explicit mentions. + * + * Zero LLM calls. Zero external dependencies beyond node: crypto. + */ + +import crypto from "node:crypto"; +import type { + PlanArtifact, + PlanEntity, + PlanRelation, + PlanEntityType, + PlanRelationType, +} from "./types.js"; + +// ─── Technology/Service detection patterns ─────────────────────────── + +/** + * Known technologies and their common names/aliases. + * Extracted from the Approach and Decisions sections. + */ +const TECH_PATTERNS: Array<{ regex: RegExp; name: string; type: PlanEntityType }> = [ + { regex: /\bRedis\b/gi, name: "Redis", type: "service" }, + { regex: /\bPostgreSQL\b/gi, name: "PostgreSQL", type: "service" }, + { regex: /\bMySQL\b/gi, name: "MySQL", type: "service" }, + { regex: /\bSQLite\b/gi, name: "SQLite", type: "service" }, + { regex: /\bMongoDB\b/gi, name: "MongoDB", type: "service" }, + { regex: /\bKafka\b/gi, name: "Kafka", type: "service" }, + { regex: /\bRabbitMQ\b/gi, name: "RabbitMQ", type: "service" }, + { regex: /\bElasticsearch\b/gi, name: "Elasticsearch", type: "service" }, + { regex: /\bQdrant\b/gi, name: "Qdrant", type: "service" }, + { regex: /\bDocker\b/gi, name: "Docker", type: "technology" }, + { regex: /\bKubernetes\b/gi, name: "Kubernetes", type: "technology" }, + { regex: /\bGraphQL\b/gi, name: "GraphQL", type: "technology" }, + { regex: /\bREST\b/gi, name: "REST", type: "technology" }, + { regex: /\bWebSocket\b/gi, name: "WebSocket", type: "technology" }, + { regex: /\bTypeScript\b/gi, name: "TypeScript", type: "technology" }, + { regex: /\bJavaScript\b/gi, name: "JavaScript", type: "technology" }, + { regex: /\bPython\b/gi, name: "Python", type: "technology" }, + { regex: /\bRust\b/gi, name: "Rust", type: "technology" }, + { regex: /\bGo(lang)?\b/gi, name: "Go", type: "technology" }, + { regex: /\bSwift\b/gi, name: "Swift", type: "technology" }, + { regex: /\bKotlin\b/gi, name: "Kotlin", type: "technology" }, + { regex: /\bReact\b/gi, name: "React", type: "technology" }, + { regex: /\bVue(\.js)?\b/gi, name: "Vue.js", type: "technology" }, + { regex: /\bAngular\b/gi, name: "Angular", type: "technology" }, + { regex: /\bNext\.js\b/gi, name: "Next.js", type: "technology" }, + { regex: /\bNode\.js\b/gi, name: "Node.js", type: "technology" }, + { regex: /\bFastify\b/gi, name: "Fastify", type: "technology" }, + { regex: /\bExpress\b/gi, name: "Express", type: "technology" }, + { regex: /\bLua\b/gi, name: "Lua", type: "technology" }, + { regex: /\bHMAC\b/gi, name: "HMAC", type: "technology" }, + { regex: /\bCAPTCHA\b/gi, name: "CAPTCHA", type: "technology" }, + { regex: /\bMFA\b/gi, name: "MFA", type: "technology" }, + { regex: /\bJWT\b/gi, name: "JWT", type: "technology" }, + { regex: /\bOAuth2?\b/gi, name: "OAuth", type: "technology" }, + { regex: /\bgRPC\b/gi, name: "gRPC", type: "technology" }, + { regex: /\bPrometheus\b/gi, name: "Prometheus", type: "technology" }, + { regex: /\bGrafana\b/gi, name: "Grafana", type: "technology" }, + { regex: /\bNAT\b/gi, name: "NAT", type: "technology" }, + { regex: /\bIPv4\b|\bIPv6\b/gi, name: "IP Networking", type: "technology" }, +]; + +// ─── Decision extraction patterns ──────────────────────────────────── + +/** + * Extract individual decision items from the Key Decisions section. + * Format is typically: + * - <decision title>: <explanation> + * or numbered items. + */ +const DECISION_PATTERN = /^[-*]\s+(.+?)(?::\s|;\s|。|\.\s|$)/gm; + +// ─── Risk extraction patterns ──────────────────────────────────────── + +/** + * Extract individual risk items from the Risks section. + */ +const RISK_PATTERN = /^[-*]\s+(.+?)(?::\s|;\s|。|\.\s|$)/gm; + +// ─── Constraint extraction ─────────────────────────────────────────── + +/** + * Extract individual constraint items from the Constraints section. + */ +const CONSTRAINT_PATTERN = /^[-*]\s+(.+?)(?::\s|;\s|。|\.\s|$)/gm; + +// ─── Out-of-Scope extraction ───────────────────────────────────────── + +const OUT_OF_SCOPE_PATTERN = /^[-*]\s+(.+?)(?::\s|;\s|。|\.\s|$)/gm; + +// ─── Main extractor ────────────────────────────────────────────────── + +export interface ExtractionOutput { + entities: PlanEntity[]; + relations: PlanRelation[]; +} + +/** + * Extract entities and relationships from a parsed PlanArtifact. + */ +export function extractFromArtifact(artifact: PlanArtifact): ExtractionOutput { + const entities: PlanEntity[] = []; + const relations: PlanRelation[] = []; + const entityMap = new Map<string, PlanEntity>(); + const planId = artifact.id; + const now = new Date().toISOString(); + + function addEntity( + type: PlanEntityType, + name: string, + description: string, + properties: Record<string, string> = {}, + ): PlanEntity { + const key = `${type}:${name}:${planId}`; + // Deduplicate within this plan + const existing = entityMap.get(key); + if (existing) return existing; + + const entity: PlanEntity = { + id: crypto.randomUUID(), + planId, + type, + name, + description: description.slice(0, 500), // Cap description length + properties, + createdAt: now, + }; + entities.push(entity); + entityMap.set(key, entity); + return entity; + } + + function addRelation( + fromEntity: PlanEntity, + toEntity: PlanEntity, + relation: PlanRelationType, + properties: Record<string, string> = {}, + ): void { + // Avoid duplicate relations + const key = `${fromEntity.id}:${toEntity.id}:${relation}`; + if (relations.some(r => `${r.fromEntityId}:${r.toEntityId}:${r.relation}` === key)) return; + + relations.push({ + id: crypto.randomUUID(), + planId, + fromEntityId: fromEntity.id, + toEntityId: toEntity.id, + relation, + properties, + createdAt: now, + }); + } + + // ── 1. Goal entity ────────────────────────────────────────────── + const goalText = artifact.sections.goal; + if (goalText) { + const goal = addEntity("goal", artifact.sections.title, goalText); + } + + // ── 2. Constraint entities ────────────────────────────────────── + const constraintsText = artifact.sections.constraints; + if (constraintsText) { + const items = extractBulletItems(constraintsText, CONSTRAINT_PATTERN); + if (items.length === 0) { + // Whole section as one constraint if no bullets + addEntity("constraint", `${artifact.id}-constraint`, constraintsText); + } else { + for (const item of items) { + addEntity("constraint", limitName(item), item); + } + } + } + + // ── 3. Decision entities ──────────────────────────────────────── + const decisionsText = artifact.sections.decisions; + if (decisionsText) { + const items = extractBulletItems(decisionsText, DECISION_PATTERN); + if (items.length === 0) { + addEntity("decision", `${artifact.id}-decision`, decisionsText); + } else { + for (const item of items) { + addEntity("decision", limitName(item), item); + } + } + } + + // ── 4. Risk entities ──────────────────────────────────────────── + const risksText = artifact.sections.risks; + if (risksText) { + const items = extractBulletItems(risksText, RISK_PATTERN); + if (items.length === 0) { + addEntity("risk", `${artifact.id}-risk`, risksText); + } else { + for (const item of items) { + addEntity("risk", limitName(item), item); + } + } + } + + // ── 5. Out-of-Scope entities ──────────────────────────────────── + const oosText = artifact.sections.outOfScope; + if (oosText) { + const items = extractBulletItems(oosText, OUT_OF_SCOPE_PATTERN); + for (const item of items) { + addEntity("out_of_scope", limitName(item), item); + } + } + + // ── 6. Technology/Service entities ────────────────────────────── + const techTexts = [artifact.sections.approach, artifact.sections.decisions].join("\n"); + const seenTech = new Set<string>(); + for (const pattern of TECH_PATTERNS) { + if (pattern.regex.test(techTexts) && !seenTech.has(pattern.name)) { + addEntity(pattern.type, pattern.name, `Mentioned in ${artifact.id}`); + seenTech.add(pattern.name); + } + } + + // ── 7. Build relationships ────────────────────────────────────── + buildRelationships(entities, relations, planId, now); + + return { entities, relations }; +} + +// ─── Helpers ───────────────────────────────────────────────────────── + +function extractBulletItems(text: string, pattern: RegExp): string[] { + const items: string[] = []; + let match; + while ((match = pattern.exec(text)) !== null) { + items.push(match[1].trim()); + } + return items; +} + +/** + * Create a short name from the first ~80 chars of a text. + */ +function limitName(text: string): string { + return text.length > 80 ? text.slice(0, 77) + "..." : text; +} + +/** + * Build implicit relationships between entities within the same plan. + * + * Heuristics: + * - Each decision "uses" technologies mentioned in its text + * - Each risk "addresses" the goal + * - Each decision can reference technologies + * - Reviewers "found" flaws + */ +function buildRelationships( + entities: PlanEntity[], + relations: PlanRelation[], + planId: string, + now: string, +): void { + const byType = new Map<PlanEntityType, PlanEntity[]>(); + for (const e of entities) { + const list = byType.get(e.type) ?? []; + list.push(e); + byType.set(e.type, list); + } + + const goals = byType.get("goal") ?? []; + const decisions = byType.get("decision") ?? []; + const risks = byType.get("risk") ?? []; + const techs = [...(byType.get("technology") ?? []), ...(byType.get("service") ?? [])]; + const constraints = byType.get("constraint") ?? []; + + function addRel( + from: PlanEntity, + to: PlanEntity, + relation: PlanRelationType, + ): void { + const key = `${from.id}:${to.id}:${relation}`; + if (relations.some(r => `${r.fromEntityId}:${r.toEntityId}:${r.relation}` === key)) return; + relations.push({ + id: crypto.randomUUID(), + planId, + fromEntityId: from.id, + toEntityId: to.id, + relation, + properties: {}, + createdAt: now, + }); + } + + // Decision → Technology (if tech appears in decision text) + for (const dec of decisions) { + for (const tech of techs) { + if (dec.description.toLowerCase().includes(tech.name.toLowerCase())) { + addRel(dec, tech, "uses"); + } + } + } + + // Goal ← Decision (decisions address the goal) + for (const goal of goals) { + for (const dec of decisions) { + addRel(dec, goal, "addresses"); + } + } + + // Risk → Goal (risks threaten the goal) + for (const goal of goals) { + for (const risk of risks) { + addRel(risk, goal, "addresses"); + } + } + + // Constraint → Decision (constraints constrain decisions) + for (const con of constraints) { + for (const dec of decisions) { + addRel(con, dec, "constrains"); + } + } + + // Decision → Technology (constrains tech choices) + for (const dec of decisions) { + for (const tech of techs) { + if (dec.description.toLowerCase().includes(tech.name.toLowerCase())) { + addRel(dec, tech, "uses"); + } + } + } +} + +/** + * Extract all text from a plan artifact suitable for embedding. + * Returns chunks keyed by section name. + */ +export function planToChunks(artifact: PlanArtifact): Array<{ + section: string; + text: string; +}> { + const chunks: Array<{ section: string; text: string }> = []; + + if (artifact.sections.goal) { + chunks.push({ section: "goal", text: artifact.sections.goal }); + } + if (artifact.sections.approach) { + chunks.push({ section: "approach", text: artifact.sections.approach }); + } + if (artifact.sections.decisions) { + chunks.push({ section: "decisions", text: artifact.sections.decisions }); + } + if (artifact.sections.constraints) { + chunks.push({ section: "constraints", text: artifact.sections.constraints }); + } + if (artifact.sections.risks) { + chunks.push({ section: "risks", text: artifact.sections.risks }); + } + if (artifact.sections.validation) { + chunks.push({ section: "validation", text: artifact.sections.validation }); + } + if (artifact.architectureAnalysis) { + chunks.push({ section: "architecture_analysis", text: artifact.architectureAnalysis }); + } + // Full plan text as one combined chunk for holistic search + const fullText = [ + `Title: ${artifact.sections.title}`, + `Goal: ${artifact.sections.goal}`, + `Approach: ${artifact.sections.approach}`, + `Decisions: ${artifact.sections.decisions}`, + artifact.architectureAnalysis ? `Architecture analysis: ${artifact.architectureAnalysis}` : "", + ].join("\n"); + chunks.push({ section: "full", text: fullText }); + + return chunks; +} diff --git a/skills-engineering/plan-reviews/src/index.ts b/skills-engineering/plan-reviews/src/index.ts new file mode 100644 index 0000000..5b45fdd --- /dev/null +++ b/skills-engineering/plan-reviews/src/index.ts @@ -0,0 +1,132 @@ +/** + * Main entry point for the plan-reviews knowledge base. + * + * Provides a high-level API that mirrors rag-gateway's capabilities + * but operates entirely in-process with zero external services: + * + * - Single JSON file replaces SQLite/PostgreSQL + * - In-memory cosine similarity replaces Qdrant + * - Structural parsing replaces LLM entity extraction + * - Embedding API (optional) is the only retained external dependency + * + * Usage: + * ```typescript + * import { PlanReviewsKB } from "@i-stack/plan-reviews-kb"; + * + * const kb = await PlanReviewsKB.init(); + * await kb.sync(); + * const results = await kb.search({ query: "Redis rate limiting" }); + * console.log(kb.formatResults(results)); + * kb.close(); + * ``` + */ + +import { loadConfig } from "./config.js"; +import type { PlanReviewsConfig } from "./config.js"; +import { PlanStore } from "./store.js"; +import { EmbeddingService } from "./embed.js"; +import { VectorIndex } from "./vector.js"; +import { SearchEngine } from "./search.js"; +import { SyncEngine } from "./sync.js"; +import type { SearchQuery, SearchResponse, SyncStats, KbStats } from "./types.js"; + +export class PlanReviewsKB { + private config: PlanReviewsConfig; + store: PlanStore; + embed: EmbeddingService; + vector: VectorIndex; + searchEngine: SearchEngine; + syncEngine: SyncEngine; + + private constructor(config: PlanReviewsConfig) { + this.config = config; + this.store = new PlanStore(config.indexPath); + this.embed = new EmbeddingService(config); + this.vector = new VectorIndex(); + this.searchEngine = new SearchEngine(this.store, this.embed, this.vector); + this.syncEngine = new SyncEngine(config, this.store, this.embed, this.vector); + } + + /** + * Initialize the knowledge base from configuration. + * Loads existing chunks from JSON cache into the in-memory vector index. + * Call `kb.sync()` after initialization to index/update plans. + */ + static async init( + overrides?: Partial<PlanReviewsConfig>, + ): Promise<PlanReviewsKB> { + const config = loadConfig(overrides); + const kb = new PlanReviewsKB(config); + + // Load existing vectors from cached chunks into memory + const chunks = kb.store.getAllChunks(); + kb.vector.load(chunks); + + return kb; + } + + /** Sync the knowledge base with .plan-reviews/ directory. */ + async sync(): Promise<SyncStats> { + return this.syncEngine.sync(); + } + + /** Full reset and re-sync. */ + async reset(): Promise<SyncStats> { + return this.syncEngine.reset(); + } + + /** Combined semantic + graph search. */ + async search(query: SearchQuery): Promise<SearchResponse> { + return this.searchEngine.search(query); + } + + /** Semantic-only search. */ + async semanticSearch(query: SearchQuery): Promise<SearchResponse["semantic"]> { + return this.searchEngine.semanticSearch(query); + } + + /** Graph-only search. */ + async graphSearch(query: SearchQuery): Promise<SearchResponse["graph"]> { + return this.searchEngine.graphSearch(query); + } + + /** Format search results as human-readable markdown. */ + formatResults(response: SearchResponse): string { + return this.searchEngine.formatResults(response); + } + + /** Get knowledge base statistics. */ + get stats(): KbStats { + return this.store.stats; + } + + /** Persist current state (called automatically by sync, but can be manual). */ + save(): void { + this.store.save(); + } + + /** Clean up (no-op in JSON mode, but kept for API compatibility). */ + close(): void { + // State is already persisted to disk + } +} + +// Re-export types for consumers +export type { PlanReviewsConfig } from "./config.js"; +export type { + SearchQuery, + SearchResponse, + SyncStats, + KbStats, + PlanArtifact, + PlanSections, + PlanEntity, + PlanRelation, + PlanEntityType, + PlanRelationType, + GraphSearchResult, + SemanticHit, + PlanResolution, + EmbeddedChunk, + SyncEvent, +} from "./types.js"; diff --git a/skills-engineering/plan-reviews/src/parser.ts b/skills-engineering/plan-reviews/src/parser.ts new file mode 100644 index 0000000..ba106ec --- /dev/null +++ b/skills-engineering/plan-reviews/src/parser.ts @@ -0,0 +1,253 @@ +/** + * Structured parser for PLAN.md and review artifacts. + * + * Unlike rag-gateway's entity extractor (which uses an LLM to extract + * entities from unstructured conversation text), this parser operates + * on the well-known structure of PLAN.md files: + * + * # Plan: <title> + * ## Goal + * ## Constraints & assumptions + * ## Approach + * ## Key decisions & tradeoffs + * ## Validation plan + * ## Risks / non-blocking open questions + * ## Out of scope + * + * And PLAN-REVIEW-LOG.md: + * - MAX_ROUNDS, Reviewers + * - Round N - <reviewer> → Flaws → VERDICT + * - Orchestrator response: Accepted / Rejected + * - Resolution + * + * No LLM calls needed — pure structural parsing. + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { + PlanSections, + PlanArtifact, + PlanResolution, +} from "./types.js"; + +// ─── PLAN.md parser ────────────────────────────────────────────────── + +const SECTION_PATTERNS: Array<{ + key: keyof PlanSections; + regex: RegExp; +}> = [ + { key: "goal", regex: /^##\s+Goal\s*$/im }, + { key: "constraints", regex: /^##\s+Constraints\s*[&]\s*assumptions\s*$/im }, + { key: "approach", regex: /^##\s+Approach\s*$/im }, + { key: "decisions", regex: /^##\s+Key\s+decisions\s*[&]\s*tradeoffs\s*$/im }, + { key: "validation", regex: /^##\s+Validation\s+plan\s*$/im }, + { key: "risks", regex: /^##\s+Risks\s*\/\s*non-blocking\s+open\s+questions\s*$/im }, + { key: "outOfScope", regex: /^##\s+Out\s+of\s+scope\s*$/im }, +]; + +/** + * Extract the title from the first H1 heading in PLAN.md content. + */ +function extractTitle(content: string): string { + const match = content.match(/^#\s+(.+?)(?:\s*\{[^}]*\})?\s*$/m); + return match ? match[1].trim() : "Untitled Plan"; +} + +/** + * Parse a PLAN.md file into structured sections. + */ +export function parsePlan(content: string): PlanSections { + const title = extractTitle(content); + + // Normalize line endings + const normalized = content.replace(/\r\n/g, "\n"); + + // Find section boundaries + const sections = new Map<keyof PlanSections, { start: number; end: number }>(); + + for (const { key, regex } of SECTION_PATTERNS) { + const match = normalized.match(regex); + if (match && match.index !== undefined) { + sections.set(key, { start: match.index + match[0].length, end: Infinity }); + } + } + + // Sort sections by their position in the document + const sorted = Array.from(sections.entries()) + .sort((a, b) => a[1].start - b[1].start); + + // Set end of each section to the start of the next one (or end of doc) + for (let i = 0; i < sorted.length; i++) { + if (i + 1 < sorted.length) { + sections.get(sorted[i][0])!.end = sorted[i + 1][1].start; + } + } + + function getSection(key: keyof PlanSections): string { + const range = sections.get(key); + if (!range) return ""; + const text = normalized.slice(range.start, range.end).trim(); + // Remove trailing section header lines that leaked in + return cleanupSection(text); + } + + return { + title, + goal: getSection("goal"), + constraints: getSection("constraints"), + approach: getSection("approach"), + decisions: getSection("decisions"), + validation: getSection("validation"), + risks: getSection("risks"), + outOfScope: getSection("outOfScope"), + }; +} + +/** + * Clean up a section body: remove any trailing section headers from other sections. + */ +function cleanupSection(text: string): string { + // Remove any remaining H2 headers that might have leaked in + return text.replace(/^##\s+.+$/gm, "").trim(); +} + +// ─── PLAN-REVIEW-LOG.md parser ────────────────────────────────────── + +/** Parsed review metadata from PLAN-REVIEW-LOG.md. */ +export interface ReviewMetadata { + resolution: PlanResolution; + reviewers: string[]; +} + +/** + * Parse PLAN-REVIEW-LOG.md to extract review metadata (resolution, reviewers). + * Handles the multi-round format with retries. + */ +export function parseReviewLog(content: string): ReviewMetadata { + const resolution = extractResolution(content); + const reviewers = extractReviewers(content); + return { resolution, reviewers }; +} + +function extractResolution(content: string): PlanResolution { + // Find the LAST ## Resolution section (after retries) + const resolutionMatches = [...content.matchAll(/^##\s+(?:Retry\s+\d+\s+)?Resolution\s*$/gim)]; + if (resolutionMatches.length === 0) return "pending"; + + // Look at content after the last resolution header + const lastMatch = resolutionMatches[resolutionMatches.length - 1]; + const afterResolution = content.slice((lastMatch.index ?? 0) + lastMatch[0].length); + + if (/approved/i.test(afterResolution.slice(0, 500))) return "approved"; + if (/deadlock/i.test(afterResolution.slice(0, 500))) return "deadlock"; + if (/failed/i.test(afterResolution.slice(0, 500))) return "failed"; + + return "pending"; +} + +function extractReviewers(content: string): string[] { + const reviewers: string[] = []; + + // Match the Reviewers: section near the top + const reviewerSection = content.match(/^Reviewers:\s*\n((?:^-\s+.+\n?)+)/m); + if (reviewerSection) { + for (const line of reviewerSection[1].split("\n")) { + const name = line.match(/^-\s+(\S+)/); + if (name) reviewers.push(name[1].toLowerCase()); + } + } + + return reviewers; +} + +// ─── Directory scanner ─────────────────────────────────────────────── + +/** + * Scan the .plan-reviews/ directory and return metadata for all plans. + */ +export function scanPlansDir(rootDir: string): PlanArtifact[] { + const plansDir = path.join(rootDir, ".plan-reviews"); + if (!fs.existsSync(plansDir)) return []; + + const artifacts: PlanArtifact[] = []; + const entries = fs.readdirSync(plansDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + // Skip hidden directories such as the JSON cache location. + if (entry.name.startsWith(".")) continue; + + const planPath = path.join(plansDir, entry.name); + const planFile = path.join(planPath, "PLAN.md"); + const reviewFile = path.join(planPath, "PLAN-REVIEW-LOG.md"); + const architectureFile = path.join(planPath, "architecture-analysis.md"); + + if (!fs.existsSync(planFile)) continue; + + try { + const planContent = fs.readFileSync(planFile, "utf-8"); + const sections = parsePlan(planContent); + + let reviewers: string[] = []; + let resolution: PlanResolution = "pending"; + + if (fs.existsSync(reviewFile)) { + const reviewContent = fs.readFileSync(reviewFile, "utf-8"); + const meta = parseReviewLog(reviewContent); + reviewers = meta.reviewers; + resolution = meta.resolution; + } + const architectureAnalysis = fs.existsSync(architectureFile) + ? fs.readFileSync(architectureFile, "utf-8") + : undefined; + + artifacts.push({ + id: entry.name, + path: planPath, + sections, + architectureAnalysis, + hasReview: fs.existsSync(reviewFile), + resolution, + reviewers, + createdAt: extractDateFromId(entry.name), + }); + } catch (err) { + console.warn(`[plan-reviews] Failed to parse ${entry.name}: ${(err as Error).message}`); + } + } + + // Sort by date, newest first + artifacts.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); + + return artifacts; +} + +/** + * Extract ISO date from plan directory name prefix (e.g. "2026-07-06-login-rate-limit"). + */ +function extractDateFromId(id: string): string { + const match = id.match(/^(\d{4}-\d{2}-\d{2})/); + return match ? match[1] : "unknown"; +} + +/** + * Check if a plan file has been modified since the given timestamp. + */ +export function getPlanMtime(planPath: string): number { + const planFile = path.join(planPath, "PLAN.md"); + const reviewFile = path.join(planPath, "PLAN-REVIEW-LOG.md"); + const architectureFile = path.join(planPath, "architecture-analysis.md"); + + let mtime = 0; + if (fs.existsSync(planFile)) { + mtime = Math.max(mtime, fs.statSync(planFile).mtimeMs); + } + if (fs.existsSync(reviewFile)) { + mtime = Math.max(mtime, fs.statSync(reviewFile).mtimeMs); + } + if (fs.existsSync(architectureFile)) { + mtime = Math.max(mtime, fs.statSync(architectureFile).mtimeMs); + } + return mtime; +} diff --git a/skills-engineering/plan-reviews/src/search.ts b/skills-engineering/plan-reviews/src/search.ts new file mode 100644 index 0000000..fa48889 --- /dev/null +++ b/skills-engineering/plan-reviews/src/search.ts @@ -0,0 +1,168 @@ +/** + * Unified search entry point for the plan-reviews knowledge base. + * + * Combines two search modes: + * 1. Semantic search — vector similarity over embedded plan chunks + * 2. Graph search — structured entity/relationship traversal + * + * Operates entirely in-process; all data comes from the in-memory PlanStore. + */ + +import type { PlanStore } from "./store.js"; +import type { EmbeddingService } from "./embed.js"; +import type { VectorIndex } from "./vector.js"; +import type { + SearchQuery, + SearchResponse, + SemanticHit, + GraphSearchResult, + PlanEntity, + PlanRelation, + PlanEntityType, +} from "./types.js"; + +export class SearchEngine { + private store: PlanStore; + private embed: EmbeddingService; + private vector: VectorIndex; + + constructor(store: PlanStore, embed: EmbeddingService, vector: VectorIndex) { + this.store = store; + this.embed = embed; + this.vector = vector; + } + + async search(query: SearchQuery): Promise<SearchResponse> { + const [semantic, graph] = await Promise.all([ + this.semanticSearch(query), + this.graphSearch(query), + ]); + return { query: query.query, semantic, graph }; + } + + async semanticSearch(query: SearchQuery): Promise<SemanticHit[]> { + if (!this.embed.isAvailable) return []; + if (this.vector.size === 0) return []; + + try { + const queryVector = await this.embed.embed(query.query); + return this.vector.search(queryVector, { + limit: query.limit ?? 5, + planId: query.planId, + scoreThreshold: 0.35, + }); + } catch (err) { + console.warn(`[plan-reviews] Semantic search failed: ${(err as Error).message}`); + return []; + } + } + + async graphSearch(query: SearchQuery): Promise<GraphSearchResult[]> { + const matchedEntities = this.store.searchEntities(query.query, { + limit: query.limit ?? 5, + entityType: query.entityType, + planId: query.planId, + }); + + if (matchedEntities.length === 0) return []; + + const entityIds = matchedEntities.map((e) => e.id); + const { entities, edges } = this.store.getSubgraph(entityIds); + + const entityMap = new Map<string, PlanEntity>(); + for (const e of entities) { + entityMap.set(e.id, e); + } + + return matchedEntities.map((matchedEntity) => { + entityMap.set(matchedEntity.id, matchedEntity); + + const matchedRelations = edges + .filter( + (edge) => + edge.fromEntityId === matchedEntity.id || + edge.toEntityId === matchedEntity.id, + ) + .map((edge) => ({ + relation: edge, + fromEntity: + entityMap.get(edge.fromEntityId) ?? + placeholderEntity(edge.fromEntityId), + toEntity: + entityMap.get(edge.toEntityId) ?? + placeholderEntity(edge.toEntityId), + })); + + return { entity: matchedEntity, relations: matchedRelations }; + }); + } + + formatResults(response: SearchResponse): string { + const lines: string[] = []; + + if (response.semantic.length > 0) { + lines.push("## Semantic Search Results"); + for (const hit of response.semantic) { + const planLabel = + hit.planId !== "" ? ` [${hit.planId}:${hit.section}]` : ""; + lines.push( + `- (score=${hit.score.toFixed(2)})${planLabel}: ${truncate(hit.text, 200)}`, + ); + } + lines.push(""); + } + + if (response.graph.length > 0) { + lines.push("## Entity Graph Results"); + const seenRelations = new Set<string>(); + + for (const r of response.graph) { + lines.push( + `### [${r.entity.type}] ${r.entity.name} (plan: ${r.entity.planId})`, + ); + if (r.entity.description) { + lines.push(` ${truncate(r.entity.description, 150)}`); + } + + for (const rel of r.relations) { + const key = `${rel.relation.id}`; + if (seenRelations.has(key)) continue; + seenRelations.add(key); + + const direction = + rel.relation.fromEntityId === r.entity.id ? "→" : "←"; + const otherEntity = + rel.relation.fromEntityId === r.entity.id + ? rel.toEntity + : rel.fromEntity; + lines.push( + ` ${direction} [${rel.relation.relation}] → [${otherEntity.type}] ${otherEntity.name}`, + ); + } + lines.push(""); + } + } + + if (response.semantic.length === 0 && response.graph.length === 0) { + lines.push("No results found."); + } + + return lines.join("\n"); + } +} + +function placeholderEntity(id: string): PlanEntity { + return { + id, + planId: "unknown", + type: "technology", + name: id, + description: "", + properties: {}, + createdAt: "", + }; +} + +function truncate(text: string, maxLen: number): string { + return text.length > maxLen ? text.slice(0, maxLen) + "..." : text; +} diff --git a/skills-engineering/plan-reviews/src/store.ts b/skills-engineering/plan-reviews/src/store.ts new file mode 100644 index 0000000..0b6ef6c --- /dev/null +++ b/skills-engineering/plan-reviews/src/store.ts @@ -0,0 +1,259 @@ +/** + * In-memory store for plan-reviews knowledge base backed by a single JSON file. + * + * Replaces the SQLite/better-sqlite3 dependency. All data lives in memory; + * the .kb-index.json file is a pure cache — always rebuildable from + * the authoritative .plan-reviews/ Markdown files. + * + * Schema (KbIndexData): + * plans — KbPlan[] metadata per plan directory + * entities — PlanEntity[] extracted from plan sections + * relations — PlanRelation[] between entities + * chunks — EmbeddedChunk[] with optional embedding vectors + * syncState — Record<planId, KbSyncState> mtime tracking + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { + KbIndexData, + KbPlan, + KbSyncState, + KbStats, + PlanEntity, + PlanRelation, + EmbeddedChunk, + PlanEntityType, + SearchQuery, +} from "./types.js"; + +// ─── Empty state factory ────────────────────────────────────────────── + +function emptyIndex(): KbIndexData { + return { + plans: [], + entities: [], + relations: [], + chunks: [], + syncState: {}, + }; +} + +// ─── Store ──────────────────────────────────────────────────────────── + +export class PlanStore { + private indexPath: string; + private data: KbIndexData; + + constructor(indexPath: string) { + this.indexPath = indexPath; + this.data = this._load(); + } + + // ── Persistence ───────────────────────────────────────────────── + + private _load(): KbIndexData { + try { + if (fs.existsSync(this.indexPath)) { + const raw = fs.readFileSync(this.indexPath, "utf-8"); + const parsed = JSON.parse(raw); + // Cautious hydration: ensure all keys exist + return { + plans: Array.isArray(parsed.plans) ? parsed.plans : [], + entities: Array.isArray(parsed.entities) ? parsed.entities : [], + relations: Array.isArray(parsed.relations) ? parsed.relations : [], + chunks: Array.isArray(parsed.chunks) ? parsed.chunks : [], + syncState: parsed.syncState && typeof parsed.syncState === "object" ? parsed.syncState : {}, + }; + } + } catch (err) { + console.warn(`[plan-reviews] Failed to load index, starting fresh: ${(err as Error).message}`); + } + return emptyIndex(); + } + + save(): void { + const dir = path.dirname(this.indexPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + // Atomic write: write to temp file, then rename + const tmp = `${this.indexPath}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(this.data, null, 2), "utf-8"); + fs.renameSync(tmp, this.indexPath); + } + + // ── Plans ──────────────────────────────────────────────────────── + + upsertPlan(plan: KbPlan): void { + const idx = this.data.plans.findIndex((p) => p.id === plan.id); + if (idx >= 0) { + this.data.plans[idx] = plan; + } else { + this.data.plans.push(plan); + } + } + + deletePlan(planId: string): void { + this.data.plans = this.data.plans.filter((p) => p.id !== planId); + this.data.entities = this.data.entities.filter((e) => e.planId !== planId); + this.data.relations = this.data.relations.filter((r) => r.planId !== planId); + this.data.chunks = this.data.chunks.filter((c) => c.planId !== planId); + delete this.data.syncState[planId]; + } + + getPlan(planId: string): KbPlan | undefined { + return this.data.plans.find((p) => p.id === planId); + } + + listPlans(): KbPlan[] { + return [...this.data.plans].sort( + (a, b) => b.createdAt.localeCompare(a.createdAt), + ); + } + + // ── Entities ───────────────────────────────────────────────────── + + clearPlanEntities(planId: string): void { + this.data.entities = this.data.entities.filter((e) => e.planId !== planId); + this.data.relations = this.data.relations.filter((r) => r.planId !== planId); + } + + upsertEntities(entities: PlanEntity[]): void { + for (const e of entities) { + const idx = this.data.entities.findIndex( + (ex) => ex.id === e.id, + ); + if (idx >= 0) { + this.data.entities[idx] = e; + } else { + this.data.entities.push(e); + } + } + } + + upsertRelations(relations: PlanRelation[]): void { + for (const r of relations) { + const idx = this.data.relations.findIndex( + (rx) => rx.id === r.id, + ); + if (idx >= 0) { + this.data.relations[idx] = r; + } else { + this.data.relations.push(r); + } + } + } + + /** + * Search entities by name or description (substring match, case-insensitive). + * Returns PlanEntity objects directly (no row mapping needed). + */ + searchEntities( + query: string, + options?: { limit?: number; entityType?: string; planId?: string }, + ): PlanEntity[] { + const lower = query.toLowerCase(); + const limit = options?.limit ?? 10; + + let results = this.data.entities.filter( + (e) => + e.name.toLowerCase().includes(lower) || + e.description.toLowerCase().includes(lower), + ); + + if (options?.entityType) { + results = results.filter((e) => e.type === options.entityType); + } + if (options?.planId) { + results = results.filter((e) => e.planId === options.planId); + } + + // Sort by relevance: exact name match first, then substring + results.sort((a, b) => { + const aNameMatch = a.name.toLowerCase() === lower ? 0 : 1; + const bNameMatch = b.name.toLowerCase() === lower ? 0 : 1; + return aNameMatch - bNameMatch; + }); + + return results.slice(0, limit); + } + + /** + * Get the 1-hop subgraph around given entity IDs. + */ + getSubgraph(entityIds: string[]): { + entities: PlanEntity[]; + edges: PlanRelation[]; + } { + if (entityIds.length === 0) return { entities: [], edges: [] }; + + const edges = this.data.relations.filter( + (r) => entityIds.includes(r.fromEntityId) || entityIds.includes(r.toEntityId), + ); + + const reachableIds = new Set(entityIds); + for (const edge of edges) { + reachableIds.add(edge.fromEntityId); + reachableIds.add(edge.toEntityId); + } + + const entities = this.data.entities.filter((e) => reachableIds.has(e.id)); + + return { entities, edges }; + } + + // ── Chunks ────────────────────────────────────────────────────── + + clearPlanChunks(planId: string): void { + this.data.chunks = this.data.chunks.filter((c) => c.planId !== planId); + } + + upsertChunks(chunks: EmbeddedChunk[]): void { + for (const c of chunks) { + const idx = this.data.chunks.findIndex((cx) => cx.id === c.id); + if (idx >= 0) { + this.data.chunks[idx] = c; + } else { + this.data.chunks.push(c); + } + } + } + + getAllChunks(): EmbeddedChunk[] { + return this.data.chunks.filter((c) => c.embedding.length > 0); + } + + // ── Sync state ────────────────────────────────────────────────── + + upsertSyncState(planId: string, planMtime: number, reviewMtime: number): void { + this.data.syncState[planId] = { + planMtime, + reviewMtime, + lastSyncedAt: new Date().toISOString(), + }; + } + + getSyncState(planId: string): KbSyncState | undefined { + return this.data.syncState[planId]; + } + + getAllSyncStates(): Record<string, KbSyncState> { + return { ...this.data.syncState }; + } + + // ── Lifecycle ─────────────────────────────────────────────────── + + truncateAll(): void { + this.data = emptyIndex(); + } + + get stats(): KbStats { + return { + plans: this.data.plans.length, + entities: this.data.entities.length, + relations: this.data.relations.length, + chunks: this.data.chunks.length, + }; + } +} diff --git a/skills-engineering/plan-reviews/src/sync.ts b/skills-engineering/plan-reviews/src/sync.ts new file mode 100644 index 0000000..ee06f94 --- /dev/null +++ b/skills-engineering/plan-reviews/src/sync.ts @@ -0,0 +1,173 @@ +/** + * Incremental sync engine for .plan-reviews/ → in-memory knowledge base. + * + * Scans the .plan-reviews/ directory, diffs against stored sync state, + * and updates only changed plans. Results persist to .kb-index.json. + */ + +import * as path from "node:path"; +import crypto from "node:crypto"; +import type { PlanReviewsConfig } from "./config.js"; +import type { PlanArtifact, EmbeddedChunk, SyncStats, KbPlan } from "./types.js"; +import { scanPlansDir, getPlanMtime } from "./parser.js"; +import { extractFromArtifact, planToChunks } from "./extractor.js"; +import type { PlanStore } from "./store.js"; +import type { EmbeddingService } from "./embed.js"; +import type { VectorIndex } from "./vector.js"; + +export class SyncEngine { + private config: PlanReviewsConfig; + private store: PlanStore; + private embed: EmbeddingService; + private vector: VectorIndex; + + constructor( + config: PlanReviewsConfig, + store: PlanStore, + embed: EmbeddingService, + vector: VectorIndex, + ) { + this.config = config; + this.store = store; + this.embed = embed; + this.vector = vector; + } + + async sync(): Promise<SyncStats> { + const stats: SyncStats = { added: 0, modified: 0, removed: 0, skipped: 0, errors: [] }; + const now = new Date().toISOString(); + + const artifacts = scanPlansDir(this.config.projectRoot); + const fsPlanIds = new Set(artifacts.map((a) => a.id)); + + const plansToIndex: PlanArtifact[] = []; + + for (const artifact of artifacts) { + const state = this.store.getSyncState(artifact.id); + const planMtime = getPlanMtime(artifact.path); + + if (!state) { + plansToIndex.push(artifact); + stats.added++; + } else if (planMtime > state.planMtime || planMtime > state.reviewMtime) { + this._removePlan(artifact.id); + plansToIndex.push(artifact); + stats.modified++; + } else { + stats.skipped++; + } + } + + // Detect removed plans + const dbPlanIds = new Set(this.store.listPlans().map((p) => p.id)); + for (const dbId of dbPlanIds) { + if (!fsPlanIds.has(dbId)) { + this._removePlan(dbId); + stats.removed++; + } + } + + for (const artifact of plansToIndex) { + try { + await this._indexArtifact(artifact, now); + } catch (err) { + const msg = `Failed to index ${artifact.id}: ${(err as Error).message}`; + stats.errors.push(msg); + console.warn(`[plan-reviews] ${msg}`); + } + } + + // Persist to JSON file + this.store.save(); + + return stats; + } + + async reset(): Promise<SyncStats> { + this.store.truncateAll(); + this.vector.clear(); + return this.sync(); + } + + private _removePlan(planId: string): void { + this.store.deletePlan(planId); + this.vector.removePlan(planId); + } + + private async _indexArtifact(artifact: PlanArtifact, now: string): Promise<void> { + // 1. Upsert plan metadata + const kbPlan: KbPlan = { + id: artifact.id, + title: artifact.sections.title, + path: artifact.path, + goal: artifact.sections.goal, + resolution: artifact.resolution, + hasReview: artifact.hasReview, + reviewers: artifact.reviewers, + createdAt: artifact.createdAt, + syncedAt: now, + }; + this.store.upsertPlan(kbPlan); + + // 2. Clear and re-extract entities/relations + this.store.clearPlanEntities(artifact.id); + const { entities, relations } = extractFromArtifact(artifact); + this.store.upsertEntities(entities); + this.store.upsertRelations(relations); + + // 3. Generate chunks and (optionally) embeddings + const rawChunks = planToChunks(artifact); + const embeddedChunks: EmbeddedChunk[] = []; + + if (this.embed.isAvailable && rawChunks.length > 0) { + const texts = rawChunks.map((c) => c.text); + try { + const vectors = await this.embed.embedBatch(texts); + for (let i = 0; i < rawChunks.length; i++) { + const chunkId = crypto.randomUUID(); + const chunk: EmbeddedChunk = { + id: chunkId, + planId: artifact.id, + section: rawChunks[i].section, + text: rawChunks[i].text, + embedding: vectors[i], + }; + embeddedChunks.push(chunk); + this.vector.add(chunkId, artifact.id, rawChunks[i].section, rawChunks[i].text, vectors[i]); + } + } catch (err) { + console.warn( + `[plan-reviews] Embedding failed for ${artifact.id}: ${(err as Error).message}`, + ); + for (const c of rawChunks) { + embeddedChunks.push({ + id: crypto.randomUUID(), + planId: artifact.id, + section: c.section, + text: c.text, + embedding: [], + }); + } + } + } else if (rawChunks.length > 0) { + for (const c of rawChunks) { + embeddedChunks.push({ + id: crypto.randomUUID(), + planId: artifact.id, + section: c.section, + text: c.text, + embedding: [], + }); + } + } + + if (embeddedChunks.length > 0) { + this.store.clearPlanChunks(artifact.id); + this.store.upsertChunks(embeddedChunks); + } + + // 4. Track sync state + const mtime = getPlanMtime(artifact.path); + this.store.upsertSyncState(artifact.id, mtime, mtime); + } +} diff --git a/skills-engineering/plan-reviews/src/types.ts b/skills-engineering/plan-reviews/src/types.ts new file mode 100644 index 0000000..1f13566 --- /dev/null +++ b/skills-engineering/plan-reviews/src/types.ts @@ -0,0 +1,195 @@ +/** + * Shared types for the plan-reviews knowledge base. + * + * Mirrors the rag-gateway types but specialized for structured + * PLAN.md artifacts rather than arbitrary conversation text. + */ + +// ─── Plan artifact types ──────────────────────────────────────────── + +/** Parsed sections of a PLAN.md file (the seven required sections). */ +export interface PlanSections { + title: string; + goal: string; + constraints: string; + approach: string; + decisions: string; + validation: string; + risks: string; + outOfScope: string; +} + +/** Metadata about a single plan-review directory. */ +export interface PlanArtifact { + /** Directory name, e.g. "2026-07-06-login-rate-limit" */ + id: string; + /** Absolute path to the plan directory */ + path: string; + /** Parsed PLAN.md sections */ + sections: PlanSections; + /** Optional PG-005 architecture analysis artifact */ + architectureAnalysis?: string; + /** Whether a PLAN-REVIEW-LOG.md exists */ + hasReview: boolean; + /** Review resolution: approved | failed | pending | deadlock */ + resolution: PlanResolution; + /** Reviewers used */ + reviewers: string[]; + /** Created date (parsed from directory name prefix) */ + createdAt: string; +} + +export type PlanResolution = "approved" | "failed" | "pending" | "deadlock"; + +// ─── Entity / Relation types (GraphRAG equivalent) ────────────────── + +/** An entity extracted from a plan or review artifact. */ +export interface PlanEntity { + id: string; + planId: string; + type: PlanEntityType; + name: string; + description: string; + properties: Record<string, string>; + createdAt: string; +} + +/** Typed entity categories for plan-specific knowledge graphs. */ +export type PlanEntityType = + | "goal" + | "constraint" + | "decision" + | "risk" + | "technology" + | "service" + | "pattern" + | "flaw" // from PLAN-REVIEW-LOG.md + | "reviewer" // from PLAN-REVIEW-LOG.md + | "out_of_scope"; + +/** A directed relationship between two plan entities. */ +export interface PlanRelation { + id: string; + planId: string; + fromEntityId: string; + toEntityId: string; + relation: PlanRelationType; + properties: Record<string, string>; + createdAt: string; +} + +export type PlanRelationType = + | "uses" + | "addresses" + | "mitigates" + | "constrains" + | "depends_on" + | "found" // reviewer found flaw + | "accepted" // orchestrator accepted flaw + | "rejected" // orchestrator rejected flaw + | "references"; + +/** Result of a graph search: matched entity plus its neighborhood. */ +export interface GraphSearchResult { + entity: PlanEntity; + relations: Array<{ + relation: PlanRelation; + fromEntity: PlanEntity; + toEntity: PlanEntity; + }>; +} + +// ─── Chunk / Embedding types ───────────────────────────────────────── + +/** A text chunk from a plan section ready for embedding. */ +export interface PlanChunk { + id: string; + planId: string; + section: string; // which section this chunk came from + text: string; +} + +/** A chunk with its embedding vector persisted. */ +export interface EmbeddedChunk extends PlanChunk { + embedding: number[]; +} + +// ─── Search types ──────────────────────────────────────────────────── + +/** Unified search query. */ +export interface SearchQuery { + query: string; + /** Maximum results per search mode */ + limit?: number; + /** Filter by plan ID */ + planId?: string; + /** Filter by entity type */ + entityType?: PlanEntityType; +} + +/** A single semantic search hit. */ +export interface SemanticHit { + chunkId: string; + planId: string; + section: string; + text: string; + score: number; +} + +/** Unified search response. */ +export interface SearchResponse { + query: string; + semantic: SemanticHit[]; + graph: GraphSearchResult[]; +} + +// ─── Sync types ────────────────────────────────────────────────────── + +export interface SyncEvent { + type: "added" | "modified" | "removed"; + planId: string; +} + +export interface SyncStats { + added: number; + modified: number; + removed: number; + skipped: number; + errors: string[]; +} + +// ─── JSON index / cache ─────────────────────────────────────────────── + +/** Complete in-memory knowledge base state persisted to .kb-index.json. */ +export interface KbIndexData { + plans: KbPlan[]; + entities: PlanEntity[]; + relations: PlanRelation[]; + chunks: EmbeddedChunk[]; + syncState: Record<string, KbSyncState>; +} + +export interface KbPlan { + id: string; + title: string; + path: string; + goal: string; + resolution: PlanResolution; + hasReview: boolean; + reviewers: string[]; + createdAt: string; + syncedAt: string; +} + +export interface KbSyncState { + planMtime: number; + reviewMtime: number; + lastSyncedAt: string; +} + +export interface KbStats { + plans: number; + entities: number; + relations: number; + chunks: number; +} diff --git a/skills-engineering/plan-reviews/src/vector.ts b/skills-engineering/plan-reviews/src/vector.ts new file mode 100644 index 0000000..b856a48 --- /dev/null +++ b/skills-engineering/plan-reviews/src/vector.ts @@ -0,0 +1,129 @@ +/** + * In-memory vector index for semantic search. + * + * Unlike rag-gateway which relies on a separate Qdrant service, + * this index keeps all vectors in memory and computes cosine + * similarity in-process. The data volume (.plan-reviews has + * typically dozens to low hundreds of chunks) makes this + * entirely feasible without external infrastructure. + * + * Vectors are loaded from the JSON cache (EmbeddedChunk[]) on init. + */ + +import type { EmbeddedChunk } from "./types.js"; + +export interface VectorSearchResult { + chunkId: string; + planId: string; + section: string; + text: string; + score: number; +} + +interface IndexEntry { + chunkId: string; + planId: string; + section: string; + text: string; + vector: number[]; +} + +export class VectorIndex { + private entries: IndexEntry[] = []; + + /** + * Load vectors from the store's chunks into memory. + */ + load(chunks: EmbeddedChunk[]): void { + this.entries = []; + for (const chunk of chunks) { + if (!chunk.embedding || chunk.embedding.length === 0) continue; + this.entries.push({ + chunkId: chunk.id, + planId: chunk.planId, + section: chunk.section, + text: chunk.text, + vector: chunk.embedding, + }); + } + } + + /** + * Add a single entry to the index (used during incremental sync). + */ + add(chunkId: string, planId: string, section: string, text: string, vector: number[]): void { + this.entries = this.entries.filter((e) => e.chunkId !== chunkId); + this.entries.push({ chunkId, planId, section, text, vector }); + } + + /** + * Remove all entries for a given planId. + */ + removePlan(planId: string): void { + this.entries = this.entries.filter((e) => e.planId !== planId); + } + + /** + * Search for the top-k most similar chunks to the given query vector. + */ + search(queryVector: number[], options?: { + limit?: number; + planId?: string; + scoreThreshold?: number; + }): VectorSearchResult[] { + const limit = options?.limit ?? 5; + const threshold = options?.scoreThreshold ?? 0.0; + const planId = options?.planId; + + const scored: Array<{ entry: IndexEntry; score: number }> = []; + + for (const entry of this.entries) { + if (planId && entry.planId !== planId) continue; + const score = cosineSimilarity(queryVector, entry.vector); + if (score >= threshold) { + scored.push({ entry, score }); + } + } + + scored.sort((a, b) => b.score - a.score); + return scored.slice(0, limit).map((s) => ({ + chunkId: s.entry.chunkId, + planId: s.entry.planId, + section: s.entry.section, + text: s.entry.text, + score: s.score, + })); + } + + get size(): number { + return this.entries.length; + } + + clear(): void { + this.entries = []; + } +} + +/** + * Compute cosine similarity between two vectors. + */ +export function cosineSimilarity(a: number[], b: number[]): number { + if (a.length !== b.length) { + throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`); + } + + let dotProduct = 0; + let normA = 0; + let normB = 0; + + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + + const denominator = Math.sqrt(normA) * Math.sqrt(normB); + if (denominator === 0) return 0; + + return dotProduct / denominator; +} diff --git a/skills-engineering/plan-reviews/src/visualize.ts b/skills-engineering/plan-reviews/src/visualize.ts new file mode 100644 index 0000000..818726b --- /dev/null +++ b/skills-engineering/plan-reviews/src/visualize.ts @@ -0,0 +1,996 @@ +/** + * Knowledge Graph Visualizer for .plan-reviews + * + * Reads .kb-index.json and generates an interactive D3.js force-directed + * graph as a self-contained HTML file. + * + * Usage: + * npx tsx src/cli.ts visualize + * npx tsx src/cli.ts visualize --output ./my-graph.html + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { KbIndexData, PlanEntity, PlanRelation } from "./types.js"; +import { loadConfig } from "./config.js"; + +// ─── Color palette for entity types ───────────────────────────────── + +const ENTITY_COLORS: Record<string, string> = { + goal: "#e74c3c", // red + constraint: "#f39c12", // orange + decision: "#2ecc71", // green + risk: "#e91e63", // pink + technology: "#3498db", // blue + service: "#9b59b6", // purple + out_of_scope: "#95a5a6", // grey + pattern: "#1abc9c", // teal + flaw: "#c0392b", // dark red + reviewer: "#8e44ad", // dark purple +}; + +const RELATION_COLORS: Record<string, string> = { + uses: "#3498db", + addresses: "#2ecc71", + mitigates: "#27ae60", + constrains: "#e67e22", + depends_on: "#9b59b6", + found: "#e74c3c", + accepted: "#27ae60", + rejected: "#c0392b", + references: "#7f8c8d", +}; + +const ENTITY_LABELS: Record<string, string> = { + goal: "目标", + constraint: "约束", + decision: "决策", + risk: "风险", + technology: "技术", + service: "服务", + out_of_scope: "范围外", + pattern: "模式", + flaw: "缺陷", + reviewer: "审查者", +}; + +const RELATION_LABELS: Record<string, string> = { + uses: "使用", + addresses: "应对", + mitigates: "缓解", + constrains: "约束", + depends_on: "依赖", + found: "发现", + accepted: "接受", + rejected: "拒绝", + references: "引用", +}; + +// ─── Main generator ───────────────────────────────────────────────── + +export interface VisualizeOptions { + /** Output HTML file path. Default: .plan-reviews/knowledge-graph.html */ + output?: string; + /** Project root. Default: cwd */ + projectRoot?: string; +} + +export function generateKnowledgeGraph(options: VisualizeOptions = {}): string { + const config = loadConfig({ projectRoot: options.projectRoot }); + const indexPath = config.indexPath; + + if (!fs.existsSync(indexPath)) { + throw new Error( + `No .kb-index.json found at ${indexPath}. Run "npm run sync" first.`, + ); + } + + const raw = fs.readFileSync(indexPath, "utf-8"); + const data: KbIndexData = JSON.parse(raw); + + if (data.entities.length === 0) { + throw new Error("No entities found in .kb-index.json. Run sync first."); + } + + const outputPath = options.output ?? path.join(config.projectRoot, ".plan-reviews", "knowledge-graph.html"); + const html = buildHtml(data, config.projectRoot); + + fs.writeFileSync(outputPath, html, "utf-8"); + return outputPath; +} + +// ─── HTML builder ──────────────────────────────────────────────────── + +function buildHtml(data: KbIndexData, _projectRoot: string): string { + const entitiesJson = JSON.stringify(data.entities); + const relationsJson = JSON.stringify(data.relations); + const plansJson = JSON.stringify(data.plans); + + return `<!DOCTYPE html> +<html lang="zh-CN"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<title>Plan Reviews — Knowledge Graph + + + + + + +
+
+
🖱 拖拽节点 | 滚轮缩放 | 右键平移 | 点击高亮邻域
+
+ + + + +`; +} diff --git a/skills-engineering/plan-reviews/tests/search.test.ts b/skills-engineering/plan-reviews/tests/search.test.ts new file mode 100644 index 0000000..edf621c --- /dev/null +++ b/skills-engineering/plan-reviews/tests/search.test.ts @@ -0,0 +1,155 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { planToChunks } from "../src/extractor.js"; +import { PlanReviewsKB } from "../src/index.js"; +import { scanPlansDir } from "../src/parser.js"; + +const tempRoots: string[] = []; + +afterEach(() => { + for (const root of tempRoots.splice(0)) { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +describe("PlanReviewsKB search", () => { + it("indexes PG-005 architecture analysis artifacts as searchable chunks", () => { + const root = makeTempProject(); + const planId = "2026-07-06-chat-rendering"; + writePlan( + root, + planId, + "Chat Rendering Change", + [ + "## Goal", + "Adjust chat rendering behavior.", + "", + "## Constraints & assumptions", + "- Architecture analysis: .plan-reviews/2026-07-06-chat-rendering/architecture-analysis.md", + "", + "## Approach", + "Update the rendering owner only.", + "", + "## Key decisions & tradeoffs", + "- Keep state ownership in the message view.", + "", + "## Validation plan", + "- Verify streaming updates.", + "", + "## Risks / non-blocking open questions", + "- Delegate callbacks may affect scroll timing.", + "", + "## Out of scope", + "- Markdown parser rewrite.", + ].join("\n"), + ); + const analysis = [ + "# 架构分析 — 2026-07-06-chat-rendering", + "", + "## 调用链", + "ChatViewController.updateStreamingMessage()", + " → NativeMarkdownView.render()", + ].join("\n"); + fs.writeFileSync( + path.join(root, ".plan-reviews", planId, "architecture-analysis.md"), + analysis, + ); + + const [artifact] = scanPlansDir(root); + expect(artifact.architectureAnalysis).toContain("NativeMarkdownView.render"); + + const chunks = planToChunks(artifact); + expect(chunks).toContainEqual({ + section: "architecture_analysis", + text: analysis, + }); + }); + + it("applies planId filtering to graph results", async () => { + const root = makeTempProject(); + writePlan( + root, + "2026-07-06-login-rate-limit", + "Login Rate Limiting With Redis", + [ + "## Goal", + "Reduce credential-stuffing risk.", + "", + "## Constraints & assumptions", + "- Redis is available as shared storage.", + "", + "## Approach", + "Use Redis with a Lua script for atomic increments.", + "", + "## Key decisions & tradeoffs", + "- Store failed login counters in Redis.", + "", + "## Validation plan", + "- Verify counters increment.", + "", + "## Risks / non-blocking open questions", + "- Redis outages reduce protection.", + "", + "## Out of scope", + "- MFA step-up.", + ].join("\n"), + ); + writePlan( + root, + "2026-07-06-password-policy", + "Password Policy", + [ + "## Goal", + "Improve password quality.", + "", + "## Constraints & assumptions", + "- Keep current login form.", + "", + "## Approach", + "Validate length and common password lists.", + "", + "## Key decisions & tradeoffs", + "- Reject known weak passwords.", + "", + "## Validation plan", + "- Verify validation errors.", + "", + "## Risks / non-blocking open questions", + "- Users may need reset guidance.", + "", + "## Out of scope", + "- Account recovery redesign.", + ].join("\n"), + ); + + const kb = await PlanReviewsKB.init({ projectRoot: root }); + await kb.sync(); + + const missingPlanResults = await kb.search({ + query: "Redis", + planId: "does-not-exist", + }); + expect(missingPlanResults.graph).toHaveLength(0); + + const otherPlanResults = await kb.search({ + query: "Redis", + planId: "2026-07-06-password-policy", + }); + expect(otherPlanResults.graph).toHaveLength(0); + }); +}); + +function makeTempProject(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "plan-reviews-kb-")); + tempRoots.push(root); + fs.mkdirSync(path.join(root, ".plan-reviews"), { recursive: true }); + return root; +} + +function writePlan(root: string, id: string, title: string, body: string): void { + const planDir = path.join(root, ".plan-reviews", id); + fs.mkdirSync(planDir, { recursive: true }); + fs.writeFileSync(path.join(planDir, "PLAN.md"), `# Plan: ${title}\n\n${body}\n`); +} diff --git a/skills-engineering/plan-reviews/tsconfig.json b/skills-engineering/plan-reviews/tsconfig.json new file mode 100644 index 0000000..155848c --- /dev/null +++ b/skills-engineering/plan-reviews/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/skills-engineering/plan-reviews/vitest.config.ts b/skills-engineering/plan-reviews/vitest.config.ts new file mode 100644 index 0000000..8881a04 --- /dev/null +++ b/skills-engineering/plan-reviews/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/skills-engineering/scripts/detect-review-clis.sh b/skills-engineering/scripts/detect-review-clis.sh new file mode 100755 index 0000000..f2b4b98 --- /dev/null +++ b/skills-engineering/scripts/detect-review-clis.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# detect-review-clis.sh — 探测可用的 reviewer CLI(codex/gemini/claude),输出 JSON。 +# 供 cross-model-review skill 的 CMR-001 使用。 +# 只做 command -v + --version,不做登录探测。 +# +# 输出格式: +# {"clis":[...],"available_count":N} +# +# 每个 CLI 对象: +# {"name":"","available":,"path":"","version":"","readonly_flag":"","noninteractive_flag":""} + +set -uo pipefail + +json_escape() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + printf '%s' "$s" +} + +probe_cli() { + local name="$1" version_flag="$2" readonly_flag="$3" noninteractive_flag="$4" + local path version + + path="$(command -v "$name" 2>/dev/null || true)" + if [[ -z "$path" ]]; then + printf '{"name":"%s","available":false}' "$name" + return 0 + fi + + version="$("$name" "$version_flag" 2>/dev/null | head -1 | tr -d '\r' || true)" + printf '{"name":"%s","available":true,"path":"%s","version":"%s","readonly_flag":"%s","noninteractive_flag":"%s"}' \ + "$name" \ + "$(json_escape "$path")" \ + "$(json_escape "$version")" \ + "$(json_escape "$readonly_flag")" \ + "$(json_escape "$noninteractive_flag")" +} + +main() { + local codex gemini claude count=0 + + codex="$(probe_cli "codex" "--version" "-s read-only" "exec")" + [[ "$codex" == *'"available":true'* ]] && ((count++)) || true + + gemini="$(probe_cli "gemini" "--version" "--approval-mode plan" "-p")" + [[ "$gemini" == *'"available":true'* ]] && ((count++)) || true + + claude="$(probe_cli "claude" "--version" "--permission-mode plan" "-p")" + [[ "$claude" == *'"available":true'* ]] && ((count++)) || true + + printf '{"clis":[%s,%s,%s],"available_count":%d}\n' \ + "$codex" "$gemini" "$claude" "$count" +} + +main "$@" diff --git a/skills-engineering/scripts/templates/agent-preamble.md.tmpl b/skills-engineering/scripts/templates/agent-preamble.md.tmpl index 67236cb..021f4b6 100644 --- a/skills-engineering/scripts/templates/agent-preamble.md.tmpl +++ b/skills-engineering/scripts/templates/agent-preamble.md.tmpl @@ -6,6 +6,8 @@ skill:logical-reasoning skill:engineering-discipline skill:problem-analysis skill:epistemic-integrity +skill:plan-grill +skill:cross-model-review --> diff --git a/sync/README.md b/sync/README.md index 9d1a699..efa0c3b 100644 --- a/sync/README.md +++ b/sync/README.md @@ -98,7 +98,6 @@ Each `env/platforms/.json` follows that platform's **official configuratio | Continue | `continue.json` | Continue `config.yaml` models | | Cursor | `cursor.json` | (no platform config needed) | | Cline | `cline.json` | (no platform config needed) | -| RAG Gateway | `rag-gateway.json` | Gateway env vars | The JSON keys map directly to the platform's native format — no field name translation needed. diff --git a/sync/sync_config.py b/sync/sync_config.py index f4f8bdd..ce3465a 100644 --- a/sync/sync_config.py +++ b/sync/sync_config.py @@ -42,14 +42,9 @@ def _auto_discover_targets() -> dict[str, SyncFn]: all_targets: dict[str, SyncFn] = dict(RENDERERS) discovered = discover_platforms() - # Platforms that are config-only (no sync target) — skip silently - _config_only = {"rag-gateway"} - for name in discovered: if name in all_targets: continue # already has a custom renderer - if name in _config_only: - continue # config-only platform, not a sync target cfg = load_platform_config(name) mcp_target = cfg.get("mcp_target") if not mcp_target: