From e5b8ec526f8c3fbe9ecdac2220ef22082283b114 Mon Sep 17 00:00:00 2001 From: Ugur Akdogan Date: Tue, 30 Jun 2026 10:17:57 +0300 Subject: [PATCH 1/9] chore: scaffold pnpm + turborepo monorepo Set up the workspace that hosts the full Solarch stack as apps/web + apps/server: pnpm workspaces, a Turborepo task pipeline, a shared tsconfig base, and unified .gitignore / .editorconfig / .nvmrc. Node >=20.19, pnpm 10, a single lockfile. --- .editorconfig | 12 + .gitignore | 31 +- .nvmrc | 1 + package.json | 41 + pnpm-lock.yaml | 11877 ++++++++++++++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + tsconfig.base.json | 10 + turbo.json | 15 + 8 files changed, 11988 insertions(+), 1 deletion(-) create mode 100644 .editorconfig create mode 100644 .nvmrc create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.base.json create mode 100644 turbo.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f15441a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index ae3897d..d07d0bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,34 @@ +# Dependencies node_modules/ +.pnpm-store/ + +# Build output dist/ +build/ +*.tsbuildinfo + +# Turborepo +.turbo/ + +# Environment (keep .env.example tracked) .env -.env.local +.env.* +!.env.example + +# Logs +*.log +npm-debug.log* +pnpm-debug.log* + +# Test / coverage +coverage/ +.nyc_output/ + +# Neo4j local data +neo4j_data/ + +# Editor / OS +.idea/ .DS_Store +Thumbs.db +*.swp diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json new file mode 100644 index 0000000..3248acb --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "solarch", + "version": "0.1.0", + "private": true, + "description": "Solarch — diagram → verified code via a strict rules engine. Monorepo: web (canvas) + server (NestJS + Neo4j).", + "license": "SEE LICENSE IN ./LICENSE", + "homepage": "https://solarch.dev", + "repository": { + "type": "git", + "url": "https://github.com/solarch-dev/solarch.git" + }, + "packageManager": "pnpm@10.0.0", + "engines": { + "node": ">=20.19.0" + }, + "scripts": { + "dev": "turbo run dev", + "build": "turbo run build", + "lint": "turbo run lint", + "dev:web": "pnpm --filter @solarch/web dev", + "dev:server": "pnpm --filter @solarch/server dev", + "build:web": "pnpm --filter @solarch/web build", + "build:server": "pnpm --filter @solarch/server build", + "test:server": "pnpm --filter @solarch/server test:unit", + "db:up": "docker compose up neo4j -d", + "db:down": "docker compose stop neo4j", + "compose:up": "docker compose up --build", + "compose:down": "docker compose down" + }, + "devDependencies": { + "turbo": "^2.5.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "sharp", + "protobufjs", + "esbuild", + "@swc/core" + ] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d7e9fd8 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,11877 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + turbo: + specifier: ^2.5.0 + version: 2.10.1 + + apps/server: + dependencies: + '@clerk/express': + specifier: ^2.1.22 + version: 2.1.33(express@5.2.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@langchain/core': + specifier: ^1.1.48 + version: 1.2.1(openai@6.45.0(zod@3.25.76)) + '@langchain/deepseek': + specifier: ^1.0.27 + version: 1.1.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))) + '@langchain/openai': + specifier: ^1.4.7 + version: 1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))) + '@nestjs/common': + specifier: ^11.0.0 + version: 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.0 + version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.0.0 + version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27) + '@nestjs/swagger': + specifier: ^11.4.4 + version: 11.4.4(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2) + '@polar-sh/sdk': + specifier: ^0.47.1 + version: 0.47.1 + '@scalar/nestjs-api-reference': + specifier: ^1.1.17 + version: 1.2.6 + '@solarch/cli': + specifier: ^0.8.0 + version: 0.8.0 + '@xenova/transformers': + specifier: ^2.17.2 + version: 2.17.2 + dotenv: + specifier: ^17.4.2 + version: 17.4.2 + helmet: + specifier: ^8.2.0 + version: 8.2.0 + neo4j-driver: + specifier: ^5.27.0 + version: 5.28.3 + nestjs-zod: + specifier: ^5.4.0 + version: 5.4.0(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.4(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@3.25.76) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9 + version: 9.39.4 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.23(@swc/core@1.15.43)(@types/node@22.20.0)(lightningcss@1.32.0) + '@nestjs/testing': + specifier: ^11.0.0 + version: 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(@nestjs/platform-express@11.1.27) + '@swc/core': + specifier: ^1.15.33 + version: 1.15.43 + '@testcontainers/neo4j': + specifier: ^12.0.0 + version: 12.0.4 + '@types/express': + specifier: ^5.0.6 + version: 5.0.6 + '@types/node': + specifier: ^22.10.0 + version: 22.20.0 + '@types/supertest': + specifier: ^6.0.2 + version: 6.0.3 + eslint: + specifier: ^9 + version: 9.39.4(jiti@1.21.7) + express: + specifier: ^5.2.1 + version: 5.2.1 + globals: + specifier: ^17.6.0 + version: 17.7.0 + supertest: + specifier: ^7.0.0 + version: 7.2.2 + testcontainers: + specifier: ^10.16.0 + version: 10.28.0 + tsx: + specifier: ^4.19.2 + version: 4.22.4 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + typescript-eslint: + specifier: ^8 + version: 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + unplugin-swc: + specifier: ^1.5.9 + version: 1.5.9(@swc/core@1.15.43)(rollup@4.62.2) + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + + apps/web: + dependencies: + '@clerk/clerk-react': + specifier: ^5.61.3 + version: 5.61.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@dagrejs/dagre': + specifier: ^3.0.0 + version: 3.0.0 + '@excalidraw/mermaid-to-excalidraw': + specifier: ^2.2.2 + version: 2.2.2 + '@fortawesome/free-solid-svg-icons': + specifier: ^7.2.0 + version: 7.3.0 + '@posthog/react': + specifier: ^1.10.1 + version: 1.10.3(@types/react@19.2.17)(posthog-js@1.396.2)(react@19.2.7) + '@radix-ui/react-collapsible': + specifier: ^1.1.12 + version: 1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-popover': + specifier: ^1.1.15 + version: 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-separator': + specifier: ^1.1.8 + version: 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': + specifier: ^1.2.4 + version: 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@tanstack/react-query': + specifier: ^5.100.12 + version: 5.101.2(react@19.2.7) + '@zxcvbn-ts/core': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-common': + specifier: ^3.0.4 + version: 3.0.4 + '@zxcvbn-ts/language-en': + specifier: ^3.0.2 + version: 3.0.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + elkjs: + specifier: ^0.11.1 + version: 0.11.1 + jszip: + specifier: ^3.10.1 + version: 3.10.1 + lucide-react: + specifier: ^1.16.0 + version: 1.22.0(react@19.2.7) + openapi-fetch: + specifier: ^0.17.0 + version: 0.17.0 + posthog-js: + specifier: ^1.386.0 + version: 1.396.2 + prism-react-renderer: + specifier: ^2.4.1 + version: 2.4.1(react@19.2.7) + react: + specifier: ^19.2.6 + version: 19.2.7 + react-dom: + specifier: ^19.2.6 + version: 19.2.7(react@19.2.7) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.17)(react@19.2.7) + react-router-dom: + specifier: ^7.15.1 + version: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 + roughjs: + specifier: ^4.6.6 + version: 4.6.6 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + zustand: + specifier: ^5.0.13 + version: 5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)) + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.6.0(jiti@1.21.7)) + '@types/node': + specifier: ^24.12.3 + version: 24.13.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0)) + autoprefixer: + specifier: ^10.5.0 + version: 10.5.2(postcss@8.5.16) + eslint: + specifier: ^10.3.0 + version: 10.6.0(jiti@1.21.7) + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.6.0(jiti@1.21.7)) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.3(eslint@10.6.0(jiti@1.21.7)) + globals: + specifier: ^17.6.0 + version: 17.7.0 + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@6.0.3) + postcss: + specifier: ^8.5.15 + version: 8.5.16 + tailwindcss: + specifier: ^3.4.19 + version: 3.4.19(tsx@4.22.4)(yaml@2.9.0) + typescript: + specifier: ~6.0.2 + version: 6.0.3 + typescript-eslint: + specifier: ^8.59.2 + version: 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + vite: + specifier: ^8.0.12 + version: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.27': + resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.27': + resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.27': + resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@balena/dockerignore@1.0.2': + resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@clerk/backend@3.8.5': + resolution: {integrity: sha512-BcMp+hXKA4Tx0fWvGh23P2hgg+58SCTmkMJip93DL+lauUNbg1ExpKzzvfSpHFyhdZkQuf4irsDvSdmV+bDpBQ==} + engines: {node: '>=20.9.0'} + + '@clerk/clerk-react@5.61.3': + resolution: {integrity: sha512-W21aNEeHtqh3xJLuW5g2ydben/1D5pSnxsl/kCnv0IY1zma7lO+aIJ7Br2bR4FKKkiu695mPnjtY+fvkQmCXBg==} + engines: {node: '>=18.17.0'} + deprecated: 'This package is no longer supported. Please use @clerk/react instead. See the upgrade guide for more info: https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3' + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + + '@clerk/express@2.1.33': + resolution: {integrity: sha512-P4TdhmHe7Dk5Df2JS3OB2PvWokEKXOgmuWWi9RIg3nNg/OiAvTgTtnSOl3kHyKX7lHSw1NotFfzjyHnmeVWjWA==} + engines: {node: '>=20.9.0'} + peerDependencies: + express: ^4.17.0 || ^5.0.0 + + '@clerk/shared@3.47.7': + resolution: {integrity: sha512-9Yv4MJFEaC7BzV0whxa4txQ4SoMu/3j1LBnI85EBykb5CcfXxIKvNX/9sjMUUySHlTOjsj7XZa5i3W5Dx02K/Q==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/shared@4.22.1': + resolution: {integrity: sha512-G7AaqgeE2rfnQzCg7V2fuHmprbt5FkMdqFBiNGUkg7yz+yjXfVjytic3TJENXI+FGK+r6vx8eTXVUIqDNQWq4g==} + engines: {node: '>=20.9.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@dagrejs/dagre@3.0.0': + resolution: {integrity: sha512-ZzhnTy1rfuoew9Ez3EIw4L2znPGnYYhfn8vc9c4oB8iw6QAsszbiU0vRhlxWPFnmmNSFAkrYeF1PhM5m4lAN0Q==} + + '@dagrejs/graphlib@4.0.1': + resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.6.0': + resolution: {integrity: sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.7.2': + resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@excalidraw/markdown-to-text@0.1.2': + resolution: {integrity: sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==} + + '@excalidraw/mermaid-to-excalidraw@2.2.2': + resolution: {integrity: sha512-5VKQq5CdRocC82vOIUpQ5ufJOVV9FpBTdHGA+ULqazeIVV+cr299877omQCibsdS3Bpitz2fsnTwnIXEmLVDSg==} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@fortawesome/fontawesome-common-types@7.3.0': + resolution: {integrity: sha512-X/vND0Y1l9fVJ9O79UgtZnXSpz4aNF3bXlDxiJAEAm6kgeSftp9wjjBPgqzazJV8YlmxfRoeXNfSCJ48sf/Hhw==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@7.3.0': + resolution: {integrity: sha512-YxI/CuwWeI3nPIoYU//vkDS+3ige/67DPZ6XwMATpYEFESzO9L8zfJOKllGRgIlpT/uebrZCcvAzp3peD7GmTw==} + engines: {node: '>=6'} + + '@grpc/grpc-js@1.14.4': + resolution: {integrity: sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@grpc/proto-loader@0.8.1': + resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} + engines: {node: '>=6'} + hasBin: true + + '@huggingface/jinja@0.2.2': + resolution: {integrity: sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==} + engines: {node: '>=18'} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@langchain/core@1.2.1': + resolution: {integrity: sha512-NNG/cC5FGuHDOAP56h0ddp8Rfk8p+othWzEK5RV9JIG6RvnF5vGa5r0AEGtKfQieed7s1kC42GuIzVOBvMBL/g==} + engines: {node: '>=20'} + + '@langchain/deepseek@1.1.3': + resolution: {integrity: sha512-2fQwIQ7OLKY/WceTaZ/dJN4p+EzDiTqvR/0RG2rm0Y6GLlxXgVlBb5qK3l+UfqmU68FgexbhLZkgUnC72THnww==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.0.0 + + '@langchain/openai@1.5.3': + resolution: {integrity: sha512-OStS2AUvy9oe/hEf/3ndBOFztUDOfuJYLNXh89m3iiJAI2Cp5Dp0n/pvpO27MO0b+VgENd+xSHVyQZ7fe+ulxg==} + engines: {node: '>=20'} + peerDependencies: + '@langchain/core': ^1.2.1 + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + + '@mermaid-js/parser@1.2.0': + resolution: {integrity: sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@nestjs/cli@11.0.23': + resolution: {integrity: sha512-2V0Bf5jz0KXhUZk3eJi9GljIyqH04otwsE/mYLbqJR+X0iiYx+6bkNJ2Qz28uHNFj1cpHgimf9xDzHkqarie0g==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.1.27': + resolution: {integrity: sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/core@11.1.27': + resolution: {integrity: sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/platform-express@11.1.27': + resolution: {integrity: sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schematics@11.1.0': + resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + peerDependencies: + prettier: ^3.0.0 + typescript: '>=4.8.2' + peerDependenciesMeta: + prettier: + optional: true + + '@nestjs/swagger@11.4.4': + resolution: {integrity: sha512-VaIo1ruV2G7b+f2zPzkBSUNy9a/WQ9sg8TLKhWlrTfg4O6U10M/PA7Xi6XMXadOVhwOqoesijba8jH3i/3adrA==} + peerDependencies: + '@fastify/static': ^8.0.0 || ^9.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/testing@11.1.27': + resolution: {integrity: sha512-I35po13UHZZeGenLWJ3QYwh77RsLau5RcFKWBZ4waVHeARpwjtC7v7n7lGh98swLQdGmZgTnbvKaZ0B5dsUIKA==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@polar-sh/sdk@0.47.1': + resolution: {integrity: sha512-fkz7wPLbqfuDmY9LxuXpE2uP2TAV6J0q/YN5hJ4UBxpjbkB0hKM6c4R35N89t83dfzMlG6EOlqOn+Rd1T6XrJQ==} + + '@posthog/core@1.39.0': + resolution: {integrity: sha512-9Xk2zndTfiO6Dd+fg2wqqA1Aw/hPUb1sArU0dL+trJSFfuz65JiuJXPBqLmIyHh+CcQOmc2b01ukMEZL/yDbgQ==} + + '@posthog/react@1.10.3': + resolution: {integrity: sha512-Qu//fGQmVlX0B9kTA3LLg67e7AYLEmeuA0Bf1qSyUM0uUILcRQGjQezhNQPLYSTakOqvXEnl6fM2iQBF6Toxrw==} + peerDependencies: + '@types/react': '>=16.8.0' + posthog-js: '>=1.257.2' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@posthog/types@1.392.0': + resolution: {integrity: sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.1': + resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} + + '@radix-ui/number@1.1.2': + resolution: {integrity: sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==} + + '@radix-ui/primitive@1.1.4': + resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} + + '@radix-ui/react-arrow@1.1.10': + resolution: {integrity: sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.14': + resolution: {integrity: sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.10': + resolution: {integrity: sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.3': + resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.4': + resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.17': + resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.2': + resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.13': + resolution: {integrity: sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.18': + resolution: {integrity: sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.4': + resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.10': + resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.2': + resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.10': + resolution: {integrity: sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.18': + resolution: {integrity: sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.17': + resolution: {integrity: sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.3.1': + resolution: {integrity: sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.12': + resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.6': + resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.6': + resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.13': + resolution: {integrity: sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.3.1': + resolution: {integrity: sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.10': + resolution: {integrity: sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.3.0': + resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.3.1': + resolution: {integrity: sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.15': + resolution: {integrity: sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.10': + resolution: {integrity: sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.2': + resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.3': + resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.3': + resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.2': + resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.2': + resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.2': + resolution: {integrity: sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.2': + resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.2': + resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.6': + resolution: {integrity: sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.2': + resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==} + + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.16': + resolution: {integrity: sha512-zIgmQTT2TV/U/SJ3N4jlIw36erH6X8ga1UNIoyrlbr0yLEbsiII/16LZ0kMxWu2A8pw0xd56rwTz5sMudy2OAw==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.62.2': + resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.2': + resolution: {integrity: sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.2': + resolution: {integrity: sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.2': + resolution: {integrity: sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.2': + resolution: {integrity: sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.2': + resolution: {integrity: sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + resolution: {integrity: sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + resolution: {integrity: sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + resolution: {integrity: sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.2': + resolution: {integrity: sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + resolution: {integrity: sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.2': + resolution: {integrity: sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + resolution: {integrity: sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + resolution: {integrity: sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + resolution: {integrity: sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + resolution: {integrity: sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + resolution: {integrity: sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.2': + resolution: {integrity: sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.2': + resolution: {integrity: sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.2': + resolution: {integrity: sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.2': + resolution: {integrity: sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + resolution: {integrity: sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + resolution: {integrity: sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.2': + resolution: {integrity: sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.2': + resolution: {integrity: sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==} + cpu: [x64] + os: [win32] + + '@scalar/client-side-rendering@0.2.6': + resolution: {integrity: sha512-wvnyBtNdYob6CdmpFqLSQoGM5q5Ra8bXyq713T/u8Nq1NXbULk1znz/LyR78u8g++0G8bkxnVzhvOf7s9FwHFw==} + engines: {node: '>=22'} + + '@scalar/helpers@0.9.0': + resolution: {integrity: sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==} + engines: {node: '>=22'} + + '@scalar/nestjs-api-reference@1.2.6': + resolution: {integrity: sha512-3GDbWXjUwZiPRRL5e0zIvZyfTapU1wLccv+oc9ai8UcgR9XTLTFa5yz5cgEhpIQrxt9sK65EouaPhsy2q0YJpQ==} + engines: {node: '>=22'} + + '@scalar/schemas@0.7.0': + resolution: {integrity: sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA==} + engines: {node: '>=22'} + + '@scalar/types@0.16.0': + resolution: {integrity: sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw==} + engines: {node: '>=22'} + + '@scalar/validation@0.6.0': + resolution: {integrity: sha512-tpmmG+/xRE2Kn9RpflU3AIyZv08v10+E1ZrJCx7z6+/91zHVxy0M73kC1LT4/8PbYNt85ywyC8+n+D99JdMcGA==} + engines: {node: '>=20'} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@solarch/ast-core@0.7.6': + resolution: {integrity: sha512-U7Ue/x7+UMV/Yg6gq2PMOzI2lsJy2mleHBBAmYCtnkJyxxL6f4MdWCxVqbeDmO60mYll/OeFaVMF0Uv/0A3akg==} + engines: {node: '>=20'} + + '@solarch/cli@0.8.0': + resolution: {integrity: sha512-s+ZfdTazMt2mrZZT8itqftOrNcm43tUgbGT7ini3jnzziKk0KiU01v2DaBNYWpYtSnQuI7/zeyd7ywTjBhb/bg==} + engines: {node: '>=20'} + hasBin: true + + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/core-darwin-arm64@1.15.43': + resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.43': + resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.43': + resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.43': + resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.43': + resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.43': + resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.43': + resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.43': + resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.43': + resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.43': + resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.43': + resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.43': + resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.43': + resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + + '@tanstack/react-query@5.101.2': + resolution: {integrity: sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==} + peerDependencies: + react: ^18 || ^19 + + '@testcontainers/neo4j@12.0.4': + resolution: {integrity: sha512-WRQfxFilXehFqq0nkeAP7xqAZavkj1YODYPiSLHrMHDoKIUxCu5fWFdigJakmvqKk86J/pup146fqZgspsuocg==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@ts-morph/common@0.29.0': + resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} + + '@turbo/darwin-64@2.10.1': + resolution: {integrity: sha512-EjfrTXVmT0r4Spv+nu1KRcvjqavCq35F5GRCvoxQi83uoX3wxQ2QTgDkSxO8O4HVXyi28dW0of/y2RFBOD4emA==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.10.1': + resolution: {integrity: sha512-nVNvaJ7aHxF5zBw8Nc9Er2Iw8A/SPAw25sqlu/63/qGfDMGdarRYrxjdM0O0XK8X8bGg3Yr93Ro7I5tJksrfgA==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.10.1': + resolution: {integrity: sha512-jaYr5GQGfW2jMkoux7/Yh+pUhKgqBM0pyAZnNTUybnVPy4qB2jP0C4B32Nmg00BYaAU3FaWr/bQ3CKKIYjdI2Q==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.10.1': + resolution: {integrity: sha512-2Wg5TBGYQjaPMJhQzYf0EEM9N5mSE3AKmWBWKz6fsjZ8dlLL4uV7X3PnwtNO1+kRYjwg34ilJwweaT8MvxZOcA==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.10.1': + resolution: {integrity: sha512-fRCK6wZiWQgE5fb+WpaBgDsHNo/fKcCoMEOms9E5Il/Bp/ec9uhsVNn0V/2gmN2hSCyFm7oKf0BZY6Lb6CDMOQ==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.10.1': + resolution: {integrity: sha512-6REIwRpmmnJdHYL+fIv2BGBC9PYd+8Ta+J53nmcHjqi46v/z+hS1sirYU5fg7Cg1r9/99dpRtSXHKTgvcLYSpg==} + cpu: [arm64] + os: [win32] + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/docker-modem@3.0.6': + resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} + + '@types/dockerode@3.3.47': + resolution: {integrity: sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==} + + '@types/dockerode@4.0.1': + resolution: {integrity: sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/long@4.0.2': + resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@22.20.0': + resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + '@types/prismjs@1.26.6': + resolution: {integrity: sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/ssh2-streams@0.1.13': + resolution: {integrity: sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA==} + + '@types/ssh2@0.5.52': + resolution: {integrity: sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==} + + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + + '@types/superagent@8.1.10': + resolution: {integrity: sha512-nbt4IWXABhW0jGmmpRzCFNlbmwCTzZ2gTUsNIr+X+ItdqPms+PAJZbWsNzpS2USqXjcoNLQcO6nXo60zcPQiIg==} + + '@types/supertest@6.0.3': + resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.62.1': + resolution: {integrity: sha512-4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.62.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.62.1': + resolution: {integrity: sha512-sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.62.1': + resolution: {integrity: sha512-yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.62.1': + resolution: {integrity: sha512-r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.62.1': + resolution: {integrity: sha512-xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.62.1': + resolution: {integrity: sha512-aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.62.1': + resolution: {integrity: sha512-ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.62.1': + resolution: {integrity: sha512-xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.62.1': + resolution: {integrity: sha512-sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.62.1': + resolution: {integrity: sha512-4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-react@6.0.3': + resolution: {integrity: sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xenova/transformers@2.17.2': + resolution: {integrity: sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + + '@zxcvbn-ts/language-common@3.0.4': + resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==} + + '@zxcvbn-ts/language-en@3.0.2': + resolution: {integrity: sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.17.0: + resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + archiver-utils@5.0.2: + resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} + engines: {node: '>= 14'} + + archiver@7.0.1: + resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} + engines: {node: '>= 14'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async-lock@1.4.1: + resolution: {integrity: sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.5.2: + resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.7.2: + resolution: {integrity: sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.9.3: + resolution: {integrity: sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.1: + resolution: {integrity: sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.5: + resolution: {integrity: sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.40: + resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.4: + resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + buildcheck@0.0.7: + resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} + engines: {node: '>=10.0.0'} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + byline@5.0.0: + resolution: {integrity: sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==} + engines: {node: '>=0.10.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001799: + resolution: {integrity: sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + compress-commons@6.0.2: + resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} + engines: {node: '>= 14'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cpu-features@0.0.10: + resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==} + engines: {node: '>=10.0.0'} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@6.0.0: + resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} + engines: {node: '>= 14'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.34.0: + resolution: {integrity: sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + docker-compose@0.24.8: + resolution: {integrity: sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==} + engines: {node: '>= 6.0.0'} + + docker-compose@1.4.2: + resolution: {integrity: sha512-rPHigTKGaEHpkUmfd69QgaOp+Os5vGJwG/Ry8lcr8W/382AmI+z/D7qoa9BybKIkqNppaIbs8RYeHSevdQjWww==} + engines: {node: '>= 6.0.0'} + + docker-modem@5.0.7: + resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==} + engines: {node: '>= 8.0'} + + dockerode@4.0.12: + resolution: {integrity: sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==} + engines: {node: '>= 8.0'} + + dockerode@5.0.1: + resolution: {integrity: sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA==} + engines: {node: '>= 14.17'} + + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.381: + resolution: {integrity: sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==} + + elkjs@0.11.1: + resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.24.1: + resolution: {integrity: sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==} + engines: {node: '>=10.13.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.2.0: + resolution: {integrity: sha512-3lGxdTXCLfe1MYfTz1y2ksAAUM4NAOP6rPEjxGJVKO7TZ5+tvHCaQWGpC4Y3IXvW3ece0Cz1cIP4FWBxOnGCTQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.49.0: + resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.3: + resolution: {integrity: sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.6.0: + resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatbuffers@1.12.0: + resolution: {integrity: sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-port@5.1.1: + resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} + engines: {node: '>=8'} + + get-port@7.2.0: + resolution: {integrity: sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg==} + engines: {node: '>=16'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.7.0: + resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + helmet@8.2.0: + resolution: {integrity: sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==} + engines: {node: '>=18.0.0'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + js-cookie@3.0.7: + resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==} + engines: {node: '>=20'} + + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + js-yaml@4.3.0: + resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + langsmith@0.7.13: + resolution: {integrity: sha512-7RWGWT0dMkImPHzOHmfNfCLQY8KiJua6q2gHFDagMSpyvnHWWi4gwkn+Z/WBDbuxpykTw06SEfElAxcu8wloKA==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + ws: '>=7' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + ws: + optional: true + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + long@4.0.0: + resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@1.22.0: + resolution: {integrity: sha512-c9o3l0PiNcgOQDW4F31BEYHudE7kgxVt3o30qMl36ZPwTxXlGB4QnLilhERvVM4uh/pl5MDyY1/gzZSYcHDtBg==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.16.0: + resolution: {integrity: sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nan@2.28.0: + resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@4.0.2: + resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + + nanoid@5.1.16: + resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} + engines: {node: ^18 || >=20} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + neo4j-driver-bolt-connection@5.28.3: + resolution: {integrity: sha512-wqHBYcU0FVRDmdsoZ+Fk0S/InYmu9/4BT6fPYh45Jimg/J7vQBUcdkiHGU7nop7HRb1ZgJmL305mJb6g5Bv35Q==} + + neo4j-driver-core@5.28.3: + resolution: {integrity: sha512-Jk+hAmjFmO5YzVH/U7FyKXigot9zmIfLz6SZQy0xfr4zfTE/S8fOYFOGqKQTHBE86HHOWH2RbTslbxIb+XtU2g==} + + neo4j-driver@5.28.3: + resolution: {integrity: sha512-k7c0wEh3HoONv1v5AyLp9/BDAbYHJhz2TZvzWstSEU3g3suQcXmKEaYBfrK2UMzxcy3bCT0DrnfRbzsOW5G/Ag==} + + nestjs-zod@5.4.0: + resolution: {integrity: sha512-dxVpy1fjfK4kp+ztK+7xQP46fpvZxkeR/jcEdIvEGh/2o71iwXuy/hrKOWSPhJ1nQXV4iBdHqMizndn2GTaXDg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-releases@2.0.50: + resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onnx-proto@4.0.4: + resolution: {integrity: sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==} + + onnxruntime-common@1.14.0: + resolution: {integrity: sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==} + + onnxruntime-node@1.14.0: + resolution: {integrity: sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==} + os: [win32, darwin, linux] + + onnxruntime-web@1.14.0: + resolution: {integrity: sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==} + + openai@6.45.0: + resolution: {integrity: sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.4: + resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + posthog-js@1.396.2: + resolution: {integrity: sha512-WFdS0JL+r/M7A9XQwIGbw1Xn6W7/V5TEmv1wgrq7GFcPxZ0I3TktNGtGQNmzARbI8nKedAHFkY9UPDDg+NTSQg==} + + preact@10.29.3: + resolution: {integrity: sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prism-react-renderer@2.4.1: + resolution: {integrity: sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==} + peerDependencies: + react: '>=16.0.0' + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + properties-reader@2.3.0: + resolution: {integrity: sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==} + engines: {node: '>=14'} + + properties-reader@3.0.1: + resolution: {integrity: sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g==} + engines: {node: '>=18'} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + protobufjs@6.11.6: + resolution: {integrity: sha512-k8BHqgPBOtrlougZZqF2uUk5Z7bN8f0wj+3e8M3hvtSv0NBAz4VBy5f6R5Nxq/l+i7mRFTgNZb2trxqTpHNY/A==} + hasBin: true + + protobufjs@7.6.4: + resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + query-selector-shadow-dom@1.0.1: + resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.18.1: + resolution: {integrity: sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.1: + resolution: {integrity: sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.62.2: + resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split-ca@1.0.1: + resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} + + ssh-remote-port-forward@1.0.4: + resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==} + + ssh2@1.17.0: + resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==} + engines: {node: '>=10.16.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swagger-ui-dist@5.32.6: + resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==} + + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + tagged-tag@1.0.0: + resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} + engines: {node: '>=20'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} + engines: {node: '>=10'} + hasBin: true + + testcontainers@10.28.0: + resolution: {integrity: sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==} + + testcontainers@12.0.4: + resolution: {integrity: sha512-QIR/8xF1+F/26cIM+9B4yyxNTbKJxAv3hygZyhPRgZ8Q2AhlPZjDdpXRuk16V37X4bgJRI3hXFhoEICMBA7Adg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.3.0: + resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} + engines: {node: '>=6.10'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-morph@28.0.0: + resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.22.4: + resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} + engines: {node: '>=18.0.0'} + hasBin: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + turbo@2.10.1: + resolution: {integrity: sha512-z9WGX2bAfElLOri8JY6pcwr+GfS18B5iGefLcvv3nwM9MoE/fPQQhpgZKTRlBciqGSDuLnfNyfP+eji8mEapQA==} + hasBin: true + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-fest@5.7.0: + resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} + engines: {node: '>=20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.62.1: + resolution: {integrity: sha512-vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + undici@8.5.0: + resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==} + engines: {node: '>=22.19.0'} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-swc@1.5.9: + resolution: {integrity: sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==} + peerDependencies: + '@swc/core': ^1.2.108 + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@14.0.1: + resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-vitals@5.3.0: + resolution: {integrity: sha512-q6LWsLatGYZp5VGBIOvbTj6JBV2nOmC8KvWztXBmwJcfFAzhwKwbOxhUH306XY3CcaZDUlSmSuNPBsCn0bFu+g==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.5.0: + resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@5.106.2: + resolution: {integrity: sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zip-stream@6.0.1: + resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} + engines: {node: '>= 14'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.27(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.27(@types/node@22.20.0)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@22.20.0) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.2.4 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@balena/dockerignore@1.0.2': {} + + '@borewit/text-codec@0.2.2': {} + + '@braintree/sanitize-url@7.1.2': {} + + '@cfworker/json-schema@4.1.1': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/types@11.1.2': {} + + '@chevrotain/utils@11.0.3': {} + + '@clerk/backend@3.8.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@clerk/shared': 4.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + standardwebhooks: 1.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.61.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@clerk/shared': 3.47.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + tslib: 2.8.1 + + '@clerk/express@2.1.33(express@5.2.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@clerk/backend': 3.8.5(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@clerk/shared': 4.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + express: 5.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/shared@3.47.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + csstype: 3.1.3 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.7 + std-env: 3.10.0 + swr: 2.3.4(react@19.2.7) + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@clerk/shared@4.22.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@tanstack/query-core': 5.101.2 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.7 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@colors/colors@1.5.0': + optional: true + + '@dagrejs/dagre@3.0.0': + dependencies: + '@dagrejs/graphlib': 4.0.1 + + '@dagrejs/graphlib@4.0.1': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0(jiti@1.21.7))': + dependencies: + eslint: 10.6.0(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@1.21.7))': + dependencies: + eslint: 9.39.4(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/config-helpers@0.6.0': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@10.2.2) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@10.0.1(eslint@10.6.0(jiti@1.21.7))': + optionalDependencies: + eslint: 10.6.0(jiti@1.21.7) + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@eslint/plugin-kit@0.7.2': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@excalidraw/markdown-to-text@0.1.2': {} + + '@excalidraw/mermaid-to-excalidraw@2.2.2': + dependencies: + '@excalidraw/markdown-to-text': 0.1.2 + '@mermaid-js/parser': 0.6.3 + mermaid: 11.16.0 + nanoid: 4.0.2 + + '@fastify/busboy@2.1.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + + '@fortawesome/fontawesome-common-types@7.3.0': {} + + '@fortawesome/free-solid-svg-icons@7.3.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.3.0 + + '@grpc/grpc-js@1.14.4': + dependencies: + '@grpc/proto-loader': 0.8.1 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.4 + yargs: 17.7.3 + + '@grpc/proto-loader@0.8.1': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.6.4 + yargs: 17.7.3 + + '@huggingface/jinja@0.2.2': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@22.20.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/confirm@5.1.21(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/core@10.3.2(@types/node@22.20.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.0) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/editor@4.2.23(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/external-editor': 1.0.3(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/expand@4.0.23(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/external-editor@1.0.3(@types/node@22.20.0)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/number@3.0.23(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/password@4.0.23(@types/node@22.20.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/prompts@7.10.1(@types/node@22.20.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.20.0) + '@inquirer/confirm': 5.1.21(@types/node@22.20.0) + '@inquirer/editor': 4.2.23(@types/node@22.20.0) + '@inquirer/expand': 4.0.23(@types/node@22.20.0) + '@inquirer/input': 4.3.1(@types/node@22.20.0) + '@inquirer/number': 3.0.23(@types/node@22.20.0) + '@inquirer/password': 4.0.23(@types/node@22.20.0) + '@inquirer/rawlist': 4.1.11(@types/node@22.20.0) + '@inquirer/search': 3.2.2(@types/node@22.20.0) + '@inquirer/select': 4.4.2(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/prompts@7.3.2(@types/node@22.20.0)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@22.20.0) + '@inquirer/confirm': 5.1.21(@types/node@22.20.0) + '@inquirer/editor': 4.2.23(@types/node@22.20.0) + '@inquirer/expand': 4.0.23(@types/node@22.20.0) + '@inquirer/input': 4.3.1(@types/node@22.20.0) + '@inquirer/number': 3.0.23(@types/node@22.20.0) + '@inquirer/password': 4.0.23(@types/node@22.20.0) + '@inquirer/rawlist': 4.1.11(@types/node@22.20.0) + '@inquirer/search': 3.2.2(@types/node@22.20.0) + '@inquirer/select': 4.4.2(@types/node@22.20.0) + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/rawlist@4.1.11(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/type': 3.0.10(@types/node@22.20.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/search@3.2.2(@types/node@22.20.0)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/select@4.4.2(@types/node@22.20.0)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.20.0) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.20.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.20.0 + + '@inquirer/type@3.0.10(@types/node@22.20.0)': + optionalDependencies: + '@types/node': 22.20.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + '@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))': + dependencies: + '@cfworker/json-schema': 4.1.1 + '@standard-schema/spec': 1.1.0 + js-tiktoken: 1.0.21 + langsmith: 0.7.13(openai@6.45.0(zod@3.25.76)) + mustache: 4.2.0 + p-queue: 6.6.2 + zod: 3.25.76 + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + - ws + + '@langchain/deepseek@1.1.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))': + dependencies: + '@langchain/core': 1.2.1(openai@6.45.0(zod@3.25.76)) + '@langchain/openai': 1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76))) + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - ws + + '@langchain/openai@1.5.3(@langchain/core@1.2.1(openai@6.45.0(zod@3.25.76)))': + dependencies: + '@langchain/core': 1.2.1(openai@6.45.0(zod@3.25.76)) + js-tiktoken: 1.0.21 + openai: 6.45.0(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - ws + + '@lukeed/csprng@1.1.0': {} + + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + + '@mermaid-js/parser@1.2.0': + dependencies: + '@chevrotain/types': 11.1.2 + + '@microsoft/tsdoc@0.16.0': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nestjs/cli@11.0.23(@swc/core@1.15.43)(@types/node@22.20.0)(lightningcss@1.32.0)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.27(@types/node@22.20.0)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@22.20.0) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0)) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0) + webpack-node-externals: 3.0.0 + optionalDependencies: + '@swc/core': 1.15.43 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/html' + - '@types/node' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - prettier + - uglify-js + - webpack-cli + + '@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/core@11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27) + + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@nestjs/platform-express@11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)': + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@11.4.4(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.16.0 + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2) + js-yaml: 4.1.1 + lodash: 4.18.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.32.6 + + '@nestjs/testing@11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(@nestjs/platform-express@11.1.27)': + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27) + + '@nestjs/throttler@6.5.0(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.27(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@oxc-project/types@0.137.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@polar-sh/sdk@0.47.1': + dependencies: + standardwebhooks: 1.0.0 + zod: 3.25.76 + + '@posthog/core@1.39.0': + dependencies: + '@posthog/types': 1.392.0 + + '@posthog/react@1.10.3(@types/react@19.2.17)(posthog-js@1.396.2)(react@19.2.7)': + dependencies: + posthog-js: 1.396.2 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@posthog/types@1.392.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.1': {} + + '@radix-ui/number@1.1.2': {} + + '@radix-ui/primitive@1.1.4': {} + + '@radix-ui/react-arrow@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collapsible@1.1.14(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-collection@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-dropdown-menu@2.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-menu': 2.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-label@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-menu@2.1.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popover@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-popper@1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-arrow': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/rect': 1.1.2 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-roving-focus@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-select@2.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/number': 1.1.2 + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-collection': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + aria-hidden: 1.2.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-separator@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-switch@1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-previous': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tabs@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-roving-focus': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-tooltip@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.4 + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-popper': 1.3.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-visually-hidden': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-previous@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/rect': 1.1.2 + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-visually-hidden@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@radix-ui/rect@1.1.2': {} + + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.16(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.2.0 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/pluginutils@5.4.0(rollup@4.62.2)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.62.2 + + '@rollup/rollup-android-arm-eabi@4.62.2': + optional: true + + '@rollup/rollup-android-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.2': + optional: true + + '@rollup/rollup-darwin-x64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.2': + optional: true + + '@scalar/client-side-rendering@0.2.6': + dependencies: + '@scalar/schemas': 0.7.0 + '@scalar/types': 0.16.0 + '@scalar/validation': 0.6.0 + + '@scalar/helpers@0.9.0': {} + + '@scalar/nestjs-api-reference@1.2.6': + dependencies: + '@scalar/client-side-rendering': 0.2.6 + + '@scalar/schemas@0.7.0': + dependencies: + '@scalar/helpers': 0.9.0 + '@scalar/validation': 0.6.0 + + '@scalar/types@0.16.0': + dependencies: + '@scalar/helpers': 0.9.0 + nanoid: 5.1.16 + type-fest: 5.7.0 + zod: 4.4.3 + + '@scalar/validation@0.6.0': {} + + '@scarf/scarf@1.4.0': {} + + '@solarch/ast-core@0.7.6': + dependencies: + ts-morph: 28.0.0 + + '@solarch/cli@0.8.0': + dependencies: + '@solarch/ast-core': 0.7.6 + chokidar: 5.0.0 + commander: 15.0.0 + picocolors: 1.1.1 + + '@stablelib/base64@1.0.1': {} + + '@standard-schema/spec@1.1.0': {} + + '@swc/core-darwin-arm64@1.15.43': + optional: true + + '@swc/core-darwin-x64@1.15.43': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.43': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.43': + optional: true + + '@swc/core-linux-arm64-musl@1.15.43': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.43': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-musl@1.15.43': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.43': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.43': + optional: true + + '@swc/core-win32-x64-msvc@1.15.43': + optional: true + + '@swc/core@1.15.43': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.27 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.43 + '@swc/core-darwin-x64': 1.15.43 + '@swc/core-linux-arm-gnueabihf': 1.15.43 + '@swc/core-linux-arm64-gnu': 1.15.43 + '@swc/core-linux-arm64-musl': 1.15.43 + '@swc/core-linux-ppc64-gnu': 1.15.43 + '@swc/core-linux-s390x-gnu': 1.15.43 + '@swc/core-linux-x64-gnu': 1.15.43 + '@swc/core-linux-x64-musl': 1.15.43 + '@swc/core-win32-arm64-msvc': 1.15.43 + '@swc/core-win32-ia32-msvc': 1.15.43 + '@swc/core-win32-x64-msvc': 1.15.43 + + '@swc/counter@0.1.3': {} + + '@swc/types@0.1.27': + dependencies: + '@swc/counter': 0.1.3 + + '@tanstack/query-core@5.101.2': {} + + '@tanstack/react-query@5.101.2(react@19.2.7)': + dependencies: + '@tanstack/query-core': 5.101.2 + react: 19.2.7 + + '@testcontainers/neo4j@12.0.4': + dependencies: + testcontainers: 12.0.4 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3(supports-color@10.2.2) + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@ts-morph/common@0.29.0': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.17 + + '@turbo/darwin-64@2.10.1': + optional: true + + '@turbo/darwin-arm64@2.10.1': + optional: true + + '@turbo/linux-64@2.10.1': + optional: true + + '@turbo/linux-arm64@2.10.1': + optional: true + + '@turbo/windows-64@2.10.1': + optional: true + + '@turbo/windows-arm64@2.10.1': + optional: true + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.20.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.20.0 + + '@types/cookiejar@2.1.5': {} + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/docker-modem@3.0.6': + dependencies: + '@types/node': 22.20.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@3.3.47': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.0 + '@types/ssh2': 1.15.5 + + '@types/dockerode@4.0.1': + dependencies: + '@types/docker-modem': 3.0.6 + '@types/node': 22.20.0 + '@types/ssh2': 1.15.5 + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/esrecurse@4.3.1': {} + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 22.20.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/long@4.0.2': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.20.0': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + '@types/prismjs@1.26.6': {} + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.20.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.20.0 + + '@types/ssh2-streams@0.1.13': + dependencies: + '@types/node': 22.20.0 + + '@types/ssh2@0.5.52': + dependencies: + '@types/node': 22.20.0 + '@types/ssh2-streams': 0.1.13 + + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + + '@types/superagent@8.1.10': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.20.0 + form-data: 4.0.6 + + '@types/supertest@6.0.3': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.10 + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 10.6.0(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/eslint-plugin@8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/type-utils': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.62.1 + eslint: 9.39.4(jiti@1.21.7) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.6.0(jiti@1.21.7) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.62.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.62.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/tsconfig-utils@8.62.1(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.6.0(jiti@1.21.7) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/type-utils@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@1.21.7) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.62.1': {} + + '@typescript-eslint/typescript-estree@8.62.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.62.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@5.9.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/typescript-estree@8.62.1(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.62.1(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.62.1(typescript@6.0.3) + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/visitor-keys': 8.62.1 + debug: 4.4.3(supports-color@10.2.2) + minimatch: 10.2.5 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.62.1 + '@typescript-eslint/types': 8.62.1 + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.62.1': + dependencies: + '@typescript-eslint/types': 8.62.1 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.2': {} + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-react@6.0.3(vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0) + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xenova/transformers@2.17.2': + dependencies: + '@huggingface/jinja': 0.2.2 + onnxruntime-web: 1.14.0 + sharp: 0.32.6 + optionalDependencies: + onnxruntime-node: 1.14.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + '@zxcvbn-ts/core@3.0.4': + dependencies: + fastest-levenshtein: 1.0.16 + + '@zxcvbn-ts/language-common@3.0.4': {} + + '@zxcvbn-ts/language-en@3.0.2': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-import-phases@1.0.4(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn-jsx@5.3.2(acorn@8.17.0): + dependencies: + acorn: 8.17.0 + + acorn@8.17.0: {} + + agent-base@7.1.4: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + archiver-utils@5.0.2: + dependencies: + glob: 10.5.0 + graceful-fs: 4.2.11 + is-stream: 2.0.1 + lazystream: 1.0.1 + lodash: 4.18.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + archiver@7.0.1: + dependencies: + archiver-utils: 5.0.2 + async: 3.2.6 + buffer-crc32: 1.0.0 + readable-stream: 4.7.0 + readdir-glob: 1.1.3 + tar-stream: 3.2.0 + zip-stream: 6.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + arg@5.0.2: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} + + async-lock@1.4.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + autoprefixer@10.5.2(postcss@8.5.16): + dependencies: + browserslist: 4.28.4 + caniuse-lite: 1.0.30001799 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.16 + postcss-value-parser: 4.2.0 + + b4a@1.8.1: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + bare-events@2.9.1: {} + + bare-fs@4.7.2: + dependencies: + bare-events: 2.9.1 + bare-path: 3.0.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.4.5 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + bare-os@3.9.3: {} + + bare-path@3.0.1: + dependencies: + bare-os: 3.9.3 + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + + bare-url@2.4.5: + dependencies: + bare-path: 3.0.1 + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.40: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.7: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.4: + dependencies: + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.381 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.4) + + buffer-crc32@1.0.0: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buildcheck@0.0.7: + optional: true + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + byline@5.0.0: {} + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001799: {} + + ccount@2.0.1: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + change-case@5.4.4: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chardet@2.2.0: {} + + check-error@2.1.3: {} + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.18.1 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + chownr@1.1.4: {} + + chrome-trace-event@1.0.4: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colorette@1.4.0: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@15.0.0: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + comment-json@5.0.0: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + component-emitter@1.3.1: {} + + compress-commons@6.0.2: + dependencies: + crc-32: 1.2.2 + crc32-stream: 6.0.0 + is-stream: 2.0.1 + normalize-path: 3.0.0 + readable-stream: 4.7.0 + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cookiejar@2.1.4: {} + + core-js@3.49.0: {} + + core-util-is@1.0.3: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + cpu-features@0.0.10: + dependencies: + buildcheck: 0.0.7 + nan: 2.28.0 + optional: true + + crc-32@1.2.2: {} + + crc32-stream@6.0.0: + dependencies: + crc-32: 1.2.2 + readable-stream: 4.7.0 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.34.0 + + cytoscape-fcose@2.2.0(cytoscape@3.34.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.34.0 + + cytoscape@3.34.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.21: {} + + debug@4.4.3(supports-color@10.2.2): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + didyoumean@1.2.2: {} + + dlv@1.1.3: {} + + docker-compose@0.24.8: + dependencies: + yaml: 2.9.0 + + docker-compose@1.4.2: + dependencies: + yaml: 2.9.0 + + docker-modem@5.0.7: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + readable-stream: 3.6.2 + split-ca: 1.0.1 + ssh2: 1.17.0 + transitivePeerDependencies: + - supports-color + + dockerode@4.0.12: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.4 + tar-fs: 2.1.5 + uuid: 10.0.0 + transitivePeerDependencies: + - supports-color + + dockerode@5.0.1: + dependencies: + '@balena/dockerignore': 1.0.2 + '@grpc/grpc-js': 1.14.4 + '@grpc/proto-loader': 0.7.15 + docker-modem: 5.0.7 + protobufjs: 7.6.4 + tar-fs: 2.1.5 + transitivePeerDependencies: + - supports-color + + dompurify@3.4.11: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv@17.4.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.381: {} + + elkjs@0.11.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.24.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.2.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-toolkit@1.49.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.6.0(jiti@1.21.7)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + eslint: 10.6.0(jiti@1.21.7) + hermes-parser: 0.25.1 + zod: 3.25.76 + zod-validation-error: 4.0.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.3(eslint@10.6.0(jiti@1.21.7)): + dependencies: + eslint: 10.6.0(jiti@1.21.7) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.6.0(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.6.0 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.2 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + eslint@9.39.4(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@10.2.2) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.17.0 + acorn-jsx: 5.3.2(acorn@8.17.0) + eslint-visitor-keys: 5.0.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-util-is-identifier-name@3.0.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + + events@3.3.0: {} + + expand-template@2.0.3: {} + + expect-type@1.4.0: {} + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-sha256@1.3.0: {} + + fast-uri@3.1.3: {} + + fastest-levenshtein@1.0.16: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.4.8: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatbuffers@1.12.0: {} + + flatted@3.4.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0)): + dependencies: + '@babel/code-frame': 7.29.7 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.8.5 + tapable: 2.3.3 + typescript: 5.9.3 + webpack: 5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0) + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@2.0.0: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-port@5.1.1: {} + + get-port@7.2.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globals@17.7.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + guid-typescript@1.0.9: {} + + hachure-fill@0.5.2: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + helmet@8.2.0: {} + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + html-url-attributes@3.0.1: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immediate@3.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + index-to-position@1.2.0: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-parser@0.2.7: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + ipaddr.js@1.9.1: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + is-decimal@2.0.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-interactive@1.0.0: {} + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.20.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jiti@1.21.7: {} + + js-cookie@3.0.7: {} + + js-levenshtein@1.1.6: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + js-yaml@4.3.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + + katex@0.16.47: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + langsmith@0.7.13(openai@6.45.0(zod@3.25.76)): + dependencies: + p-queue: 6.6.2 + optionalDependencies: + openai: 6.45.0(zod@3.25.76) + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lie@3.3.0: + dependencies: + immediate: 3.0.6 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + load-tsconfig@0.2.5: {} + + loader-runner@4.3.2: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-es@4.18.1: {} + + lodash.camelcase@4.3.0: {} + + lodash.merge@4.6.2: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + long@4.0.0: {} + + long@5.3.2: {} + + longest-streak@3.1.0: {} + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@1.22.0(react@19.2.7): + dependencies: + react: 19.2.7 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + + marked@16.4.2: {} + + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.2 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + mermaid@11.16.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.2.0 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.34.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.0) + cytoscape-fcose: 2.2.0(cytoscape@3.34.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.21 + dompurify: 3.4.11 + es-toolkit: 1.49.0 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.3.0 + uuid: 14.0.1 + + methods@1.1.2: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3(supports-color@10.2.2) + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.7 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mkdirp-classic@0.5.3: {} + + mkdirp@1.0.4: {} + + mkdirp@3.0.1: {} + + ms@2.1.3: {} + + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mustache@4.2.0: {} + + mute-stream@2.0.0: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nan@2.28.0: + optional: true + + nanoid@3.3.15: {} + + nanoid@4.0.2: {} + + nanoid@5.1.16: {} + + napi-build-utils@2.0.0: {} + + natural-compare@1.4.0: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + neo4j-driver-bolt-connection@5.28.3: + dependencies: + buffer: 6.0.3 + neo4j-driver-core: 5.28.3 + string_decoder: 1.3.0 + + neo4j-driver-core@5.28.3: {} + + neo4j-driver@5.28.3: + dependencies: + neo4j-driver-bolt-connection: 5.28.3 + neo4j-driver-core: 5.28.3 + rxjs: 7.8.2 + + nestjs-zod@5.4.0(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.4.4(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@3.25.76): + dependencies: + '@nestjs/common': 11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 3.25.76 + optionalDependencies: + '@nestjs/swagger': 11.4.4(@nestjs/common@11.1.27(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.27)(reflect-metadata@0.2.2) + + node-abi@3.92.0: + dependencies: + semver: 7.8.5 + + node-abort-controller@3.1.1: {} + + node-addon-api@6.1.0: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.18.1 + + node-releases@2.0.50: {} + + normalize-path@3.0.0: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onnx-proto@4.0.4: + dependencies: + protobufjs: 6.11.6 + + onnxruntime-common@1.14.0: {} + + onnxruntime-node@1.14.0: + dependencies: + onnxruntime-common: 1.14.0 + optional: true + + onnxruntime-web@1.14.0: + dependencies: + flatbuffers: 1.12.0 + guid-typescript: 1.0.9 + long: 4.0.0 + onnx-proto: 4.0.4 + onnxruntime-common: 1.14.0 + platform: 1.3.6 + + openai@6.45.0(zod@3.25.76): + optionalDependencies: + zod: 3.25.76 + + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-typescript-helpers@0.1.0: {} + + openapi-typescript@7.13.0(typescript@6.0.3): + dependencies: + '@redocly/openapi-core': 1.34.16(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 6.0.3 + yargs-parser: 21.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-finally@1.0.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + package-json-from-dist@1.0.1: {} + + package-manager-detector@1.6.0: {} + + pako@1.0.11: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.7 + index-to-position: 1.2.0 + type-fest: 4.41.0 + + parseurl@1.3.3: {} + + path-browserify@1.0.1: {} + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.1 + minipass: 7.1.3 + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + platform@1.3.6: {} + + pluralize@8.0.0: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss-import@15.1.0(postcss@8.5.16): + dependencies: + postcss: 8.5.16 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.12 + + postcss-js@4.1.0(postcss@8.5.16): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.16 + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.16)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.16 + tsx: 4.22.4 + yaml: 2.9.0 + + postcss-nested@6.2.0(postcss@8.5.16): + dependencies: + postcss: 8.5.16 + postcss-selector-parser: 6.1.4 + + postcss-selector-parser@6.1.4: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + posthog-js@1.396.2: + dependencies: + '@posthog/core': 1.39.0 + '@posthog/types': 1.392.0 + core-js: 3.49.0 + dompurify: 3.4.11 + fflate: 0.4.8 + preact: 10.29.3 + query-selector-shadow-dom: 1.0.1 + web-vitals: 5.3.0 + + preact@10.29.3: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.92.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + prism-react-renderer@2.4.1(react@19.2.7): + dependencies: + '@types/prismjs': 1.26.6 + clsx: 2.1.1 + react: 19.2.7 + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + properties-reader@2.3.0: + dependencies: + mkdirp: 1.0.4 + + properties-reader@3.0.1: + dependencies: + '@kwsites/file-exists': 1.1.1 + mkdirp: 3.0.1 + transitivePeerDependencies: + - supports-color + + property-information@7.2.0: {} + + protobufjs@6.11.6: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/long': 4.0.2 + '@types/node': 22.20.0 + long: 4.0.0 + + protobufjs@7.6.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.1 + '@types/node': 22.20.0 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + query-selector-shadow-dom@1.0.1: {} + + queue-microtask@1.2.3: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-markdown@10.1.0(@types/react@19.2.17)(react@19.2.7): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.17 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.7 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) + react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) + use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + react-router-dom@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + react-router@7.18.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + cookie: 1.1.1 + react: 19.2.7 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + + react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + get-nonce: 1.0.1 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + react@19.2.7: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.9 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + + reflect-metadata@0.2.2: {} + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: {} + + reusify@1.1.0: {} + + robust-predicates@3.0.3: {} + + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + + rollup@4.62.2: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.2 + '@rollup/rollup-android-arm64': 4.62.2 + '@rollup/rollup-darwin-arm64': 4.62.2 + '@rollup/rollup-darwin-x64': 4.62.2 + '@rollup/rollup-freebsd-arm64': 4.62.2 + '@rollup/rollup-freebsd-x64': 4.62.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.2 + '@rollup/rollup-linux-arm-musleabihf': 4.62.2 + '@rollup/rollup-linux-arm64-gnu': 4.62.2 + '@rollup/rollup-linux-arm64-musl': 4.62.2 + '@rollup/rollup-linux-loong64-gnu': 4.62.2 + '@rollup/rollup-linux-loong64-musl': 4.62.2 + '@rollup/rollup-linux-ppc64-gnu': 4.62.2 + '@rollup/rollup-linux-ppc64-musl': 4.62.2 + '@rollup/rollup-linux-riscv64-gnu': 4.62.2 + '@rollup/rollup-linux-riscv64-musl': 4.62.2 + '@rollup/rollup-linux-s390x-gnu': 4.62.2 + '@rollup/rollup-linux-x64-gnu': 4.62.2 + '@rollup/rollup-linux-x64-musl': 4.62.2 + '@rollup/rollup-openbsd-x64': 4.62.2 + '@rollup/rollup-openharmony-arm64': 4.62.2 + '@rollup/rollup-win32-arm64-msvc': 4.62.2 + '@rollup/rollup-win32-ia32-msvc': 4.62.2 + '@rollup/rollup-win32-x64-gnu': 4.62.2 + '@rollup/rollup-win32-x64-msvc': 4.62.2 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + 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.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.2: {} + + setimmediate@1.0.5: {} + + setprototypeof@1.2.0: {} + + sharp@0.32.6: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + node-addon-api: 6.1.0 + prebuild-install: 7.1.3 + semver: 7.8.5 + simple-get: 4.0.1 + tar-fs: 3.1.3 + tunnel-agent: 0.6.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + 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 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + sonner@2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + split-ca@1.0.1: {} + + ssh-remote-port-forward@1.0.4: + dependencies: + '@types/ssh2': 0.5.52 + ssh2: 1.17.0 + + ssh2@1.17.0: + dependencies: + asn1: 0.2.6 + bcrypt-pbkdf: 1.0.2 + optionalDependencies: + cpu-features: 0.0.10 + nan: 2.28.0 + + stackback@0.0.2: {} + + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + statuses@2.0.2: {} + + std-env@3.10.0: {} + + streamsearch@1.1.0: {} + + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: {} + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + stylis@4.4.0: {} + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3(supports-color@10.2.2) + fast-safe-stringify: 2.1.1 + form-data: 4.0.6 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.3 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swagger-ui-dist@5.32.6: + dependencies: + '@scarf/scarf': 1.4.0 + + swr@2.3.4(react@19.2.7): + dependencies: + dequal: 2.0.3 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + + symbol-observable@4.0.0: {} + + tagged-tag@1.0.0: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@3.4.19(tsx@4.22.4)(yaml@2.9.0): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.16 + postcss-import: 15.1.0(postcss@8.5.16) + postcss-js: 4.1.0(postcss@8.5.16) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.16)(tsx@4.22.4)(yaml@2.9.0) + postcss-nested: 6.2.0(postcss@8.5.16) + postcss-selector-parser: 6.1.4 + resolve: 1.22.12 + sucrase: 3.35.1 + transitivePeerDependencies: + - tsx + - yaml + + tapable@2.3.3: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.7.2 + bare-path: 3.0.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.7.2 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + + terser-webpack-plugin@5.6.1(@swc/core@1.15.43)(lightningcss@1.32.0)(webpack@5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0)): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.48.0 + webpack: 5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0) + optionalDependencies: + '@swc/core': 1.15.43 + lightningcss: 1.32.0 + + terser@5.48.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.17.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + testcontainers@10.28.0: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 3.3.47 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3(supports-color@10.2.2) + docker-compose: 0.24.8 + dockerode: 4.0.12 + get-port: 7.2.0 + proper-lockfile: 4.1.2 + properties-reader: 2.3.0 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.3 + tmp: 0.2.7 + undici: 5.29.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + testcontainers@12.0.4: + dependencies: + '@balena/dockerignore': 1.0.2 + '@types/dockerode': 4.0.1 + archiver: 7.0.1 + async-lock: 1.4.1 + byline: 5.0.0 + debug: 4.4.3(supports-color@10.2.2) + docker-compose: 1.4.2 + dockerode: 5.0.1 + get-port: 5.1.1 + proper-lockfile: 4.1.2 + properties-reader: 3.0.1 + ssh-remote-port-forward: 1.0.4 + tar-fs: 3.1.3 + tmp: 0.2.7 + undici: 8.5.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tmp@0.2.7: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + ts-dedent@2.3.0: {} + + ts-interface-checker@0.1.13: {} + + ts-morph@28.0.0: + dependencies: + '@ts-morph/common': 0.29.0 + code-block-writer: 13.0.3 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.24.1 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.22.4: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + turbo@2.10.1: + optionalDependencies: + '@turbo/darwin-64': 2.10.1 + '@turbo/darwin-arm64': 2.10.1 + '@turbo/linux-64': 2.10.1 + '@turbo/linux-arm64': 2.10.1 + '@turbo/windows-64': 2.10.1 + '@turbo/windows-arm64': 2.10.1 + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@4.41.0: {} + + type-fest@5.7.0: + dependencies: + tagged-tag: 1.0.0 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript-eslint@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3))(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/parser': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@6.0.3) + '@typescript-eslint/utils': 8.62.1(eslint@10.6.0(jiti@1.21.7))(typescript@6.0.3) + eslint: 10.6.0(jiti@1.21.7) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + typescript-eslint@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.62.1(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.62.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.62.1(eslint@9.39.4(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.39.4(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + typescript@6.0.3: {} + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici-types@7.18.2: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + undici@8.5.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin-swc@1.5.9(@swc/core@1.15.43)(rollup@4.62.2): + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.62.2) + '@swc/core': 1.15.43 + load-tsconfig: 0.2.5 + unplugin: 2.3.11 + transitivePeerDependencies: + - rollup + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.17.0 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.2.3(browserslist@4.28.4): + dependencies: + browserslist: 4.28.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js-replace@1.0.1: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.7 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.17 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + util-deprecate@1.0.2: {} + + uuid@10.0.0: {} + + uuid@14.0.1: {} + + vary@1.1.2: {} + + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite-node@2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@10.2.2) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.16 + rollup: 4.62.2 + optionalDependencies: + '@types/node': 22.20.0 + fsevents: 2.3.3 + lightningcss: 1.32.0 + terser: 5.48.0 + + vite@8.1.0(@types/node@24.13.2)(esbuild@0.28.1)(jiti@1.21.7)(terser@5.48.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.2 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 1.21.7 + terser: 5.48.0 + tsx: 4.22.4 + yaml: 2.9.0 + + vitest@2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@10.2.2) + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + vite-node: 2.1.9(@types/node@22.20.0)(lightningcss@1.32.0)(terser@5.48.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.20.0 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-vitals@5.3.0: {} + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.5.0: {} + + webpack-virtual-modules@0.6.2: {} + + webpack@5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.17.0 + acorn-import-phases: 1.0.4(acorn@8.17.0) + browserslist: 4.28.4 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.1 + es-module-lexer: 2.2.0 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(@swc/core@1.15.43)(lightningcss@1.32.0)(webpack@5.106.2(@swc/core@1.15.43)(lightningcss@1.32.0)) + watchpack: 2.5.2 + webpack-sources: 3.5.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml-ast-parser@0.0.43: {} + + yaml@2.9.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zip-stream@6.0.1: + dependencies: + archiver-utils: 5.0.2 + compress-commons: 6.0.2 + readable-stream: 4.7.0 + + zod-validation-error@4.0.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zod@4.4.3: {} + + zustand@5.0.14(@types/react@19.2.17)(react@19.2.7)(use-sync-external-store@1.6.0(react@19.2.7)): + optionalDependencies: + '@types/react': 19.2.17 + react: 19.2.7 + use-sync-external-store: 1.6.0(react@19.2.7) + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..06b6051 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "apps/*" diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..d71d1b9 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Solarch base", + "compilerOptions": { + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000..0ffc784 --- /dev/null +++ b/turbo.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "stream", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"] + }, + "lint": {}, + "dev": { + "cache": false, + "persistent": true + } + } +} From 2fc53f1cfe09add81d08b76a7c34e8e54eb33c83 Mon Sep 17 00:00:00 2001 From: Ugur Akdogan Date: Tue, 30 Jun 2026 10:17:57 +0300 Subject: [PATCH 2/9] feat: bring the full application stack into the monorepo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Import the canvas web app (Vite + React 19) as apps/web and the graph backend (NestJS + Neo4j) as apps/server. Clean import — no prior private history. - web: drop the Linux-only @rolldown native binding so installs are cross-platform - server: resolve the @solarch/cli fill engine from the published npm package (require.resolve), with a sibling solarch-tools checkout as a dev fallback - server: make the listen host configurable (HOST env) for containerized runs --- apps/server/.env.example | 41 + apps/server/.gitignore | 47 + apps/server/Dockerfile | 35 + apps/server/README.md | 154 + apps/server/deploy/Caddyfile | 45 + .../deploy/apache-app.solarch.dev-le-ssl.conf | 42 + .../server/deploy/apache-app.solarch.dev.conf | 51 + apps/server/deploy/placeholder-index.html | 381 ++ apps/server/deploy/solarch-backend.service | 25 + .../deploy/solarch-neo4j-backup.service | 9 + apps/server/deploy/solarch-neo4j-backup.timer | 11 + apps/server/docker-compose.yml | 30 + apps/server/docker-entrypoint.sh | 16 + apps/server/docs/ops/backup-restore.md | 77 + apps/server/docs/ops/deploy.md | 110 + .../2026-05-21-node-types-implementation.md | 3334 +++++++++++ ...26-05-22-node-enrichment-implementation.md | 987 ++++ ...26-05-22-phase4-graphrag-implementation.md | 1274 ++++ ...2026-05-22-tabs-contexts-implementation.md | 1134 ++++ .../specs/2026-05-21-node-types-design.md | 614 ++ .../2026-05-22-node-enrichment-design.md | 319 + .../2026-05-22-phase4-graphrag-design.md | 163 + .../specs/2026-05-22-tabs-contexts-design.md | 119 + .../2026-06-12-graph-revision-and-cli-push.md | 129 + .../superpowers/plans/2026-06-29-api-docs.md | 392 ++ .../2026-06-29-native-api-reference-planA.md | 144 + .../specs/2026-06-29-api-docs-test-design.md | 194 + .../2026-06-29-native-api-reference-design.md | 92 + apps/server/eslint.config.mjs | 20 + apps/server/nest-cli.json | 8 + apps/server/package.json | 72 + apps/server/scripts/neo4j-backup.env.example | 12 + apps/server/scripts/neo4j-backup.sh | 97 + apps/server/scripts/neo4j-restore.sh | 49 + .../src/ai/ai-idempotency.store.spec.ts | 19 + apps/server/src/ai/ai-idempotency.store.ts | 43 + apps/server/src/ai/ai.controller.ts | 203 + apps/server/src/ai/ai.module.ts | 18 + apps/server/src/ai/ai.service.spec.ts | 211 + apps/server/src/ai/ai.service.ts | 874 +++ apps/server/src/ai/dto/chat-response.dto.ts | 30 + apps/server/src/ai/dto/chat.dto.ts | 26 + .../src/ai/prompts/system-prompt.spec.ts | 77 + apps/server/src/ai/prompts/system-prompt.ts | 156 + apps/server/src/ai/providers/llm.factory.ts | 85 + .../ai/tools/apply-architecture-graph.tool.ts | 31 + apps/server/src/ai/tools/create-edge.tool.ts | 34 + apps/server/src/ai/tools/create-node.tool.ts | 31 + apps/server/src/ai/tools/delete-edge.tool.ts | 16 + apps/server/src/ai/tools/delete-node.tool.ts | 16 + apps/server/src/ai/tools/get-node.tool.ts | 20 + apps/server/src/ai/tools/update-node.tool.ts | 25 + apps/server/src/app.module.ts | 47 + apps/server/src/auth/access.ts | 22 + .../src/auth/api-keys/api-keys.controller.ts | 77 + .../src/auth/api-keys/api-keys.repository.ts | 77 + .../src/auth/api-keys/api-keys.service.ts | 65 + apps/server/src/auth/auth.module.ts | 39 + apps/server/src/auth/auth.types.ts | 12 + apps/server/src/auth/clerk-auth.guard.spec.ts | 125 + apps/server/src/auth/clerk-auth.guard.ts | 106 + .../server/src/auth/current-auth.decorator.ts | 10 + apps/server/src/auth/guest-cleanup.service.ts | 65 + apps/server/src/auth/guest-token.spec.ts | 44 + apps/server/src/auth/guest-token.ts | 56 + apps/server/src/auth/guest.controller.ts | 35 + apps/server/src/auth/project-access.guard.ts | 40 + apps/server/src/auth/public.decorator.ts | 6 + apps/server/src/billing/billing.controller.ts | 101 + apps/server/src/billing/billing.module.ts | 12 + .../src/billing/billing.service.spec.ts | 147 + apps/server/src/billing/billing.service.ts | 225 + apps/server/src/billing/entitlements.spec.ts | 41 + apps/server/src/billing/entitlements.ts | 56 + apps/server/src/billing/polar.client.spec.ts | 89 + apps/server/src/billing/polar.client.ts | 150 + .../src/billing/subscription.repository.ts | 124 + apps/server/src/codegen/__fixtures__/load.ts | 32 + .../codegen/__fixtures__/realistic-graph.json | 5188 +++++++++++++++++ apps/server/src/codegen/api-doc.spec.ts | 100 + apps/server/src/codegen/cardinality.ts | 40 + .../src/codegen/codegen-assembly.spec.ts | 158 + .../codegen/codegen-deps-warmup.service.ts | 33 + apps/server/src/codegen/codegen-fill-deps.ts | 78 + .../src/codegen/codegen-fill.service.spec.ts | 76 + .../src/codegen/codegen-fill.service.ts | 193 + .../src/codegen/codegen-tsc.gate.test.ts | 86 + .../src/codegen/codegen.controller.spec.ts | 124 + apps/server/src/codegen/codegen.controller.ts | 346 ++ apps/server/src/codegen/codegen.module.ts | 19 + .../src/codegen/codegen.service.spec.ts | 1006 ++++ apps/server/src/codegen/codegen.service.ts | 841 +++ apps/server/src/codegen/codegen.version.ts | 45 + apps/server/src/codegen/contract-lint.spec.ts | 236 + apps/server/src/codegen/contract-lint.ts | 152 + apps/server/src/codegen/dto/codegen.dto.ts | 13 + .../nestjs/api-gateway.emitter.spec.ts | 287 + .../emitters/nestjs/api-gateway.emitter.ts | 293 + .../emitters/nestjs/cache.emitter.spec.ts | 161 + .../codegen/emitters/nestjs/cache.emitter.ts | 151 + .../nestjs/controller.emitter.spec.ts | 608 ++ .../emitters/nestjs/controller.emitter.ts | 542 ++ .../emitters/nestjs/dto.emitter.spec.ts | 309 + .../codegen/emitters/nestjs/dto.emitter.ts | 266 + .../emitters/nestjs/entity-synthesis.spec.ts | 429 ++ .../emitters/nestjs/entity-synthesis.ts | 479 ++ .../emitters/nestjs/enum.emitter.spec.ts | 159 + .../codegen/emitters/nestjs/enum.emitter.ts | 143 + .../nestjs/event-handler.emitter.spec.ts | 281 + .../emitters/nestjs/event-handler.emitter.ts | 266 + .../nestjs/exception-synthesis.spec.ts | 56 + .../emitters/nestjs/exception-synthesis.ts | 89 + .../emitters/nestjs/exception.emitter.spec.ts | 181 + .../emitters/nestjs/exception.emitter.ts | 168 + .../nestjs/external-service.emitter.spec.ts | 319 + .../nestjs/external-service.emitter.ts | 219 + .../src/codegen/emitters/nestjs/index.ts | 77 + .../nestjs/message-queue.emitter.spec.ts | 225 + .../emitters/nestjs/message-queue.emitter.ts | 183 + .../nestjs/middleware.emitter.spec.ts | 232 + .../emitters/nestjs/middleware.emitter.ts | 127 + .../nestjs/migration-runner.emitter.ts | 157 + .../emitters/nestjs/model.emitter.spec.ts | 348 ++ .../codegen/emitters/nestjs/model.emitter.ts | 300 + .../emitters/nestjs/module.emitter.spec.ts | 440 ++ .../codegen/emitters/nestjs/module.emitter.ts | 357 ++ .../nestjs/orchestrator.emitter.spec.ts | 312 + .../emitters/nestjs/orchestrator.emitter.ts | 251 + .../nestjs/repository.emitter.spec.ts | 554 ++ .../emitters/nestjs/repository.emitter.ts | 338 ++ .../emitters/nestjs/scaffold.emitter.spec.ts | 736 +++ .../emitters/nestjs/scaffold.emitter.ts | 1319 +++++ .../nestjs/service-spec.emitter.spec.ts | 326 ++ .../emitters/nestjs/service-spec.emitter.ts | 289 + .../emitters/nestjs/service.emitter.spec.ts | 616 ++ .../emitters/nestjs/service.emitter.ts | 406 ++ .../emitters/nestjs/sql-type-map.spec.ts | 160 + .../codegen/emitters/nestjs/sql-type-map.ts | 206 + .../emitters/nestjs/stub.emitter.spec.ts | 196 + .../codegen/emitters/nestjs/stub.emitter.ts | 145 + .../nestjs/surgical-plan.emitter.spec.ts | 216 + .../emitters/nestjs/surgical-plan.emitter.ts | 295 + .../emitters/nestjs/table.emitter.spec.ts | 289 + .../codegen/emitters/nestjs/table.emitter.ts | 339 ++ .../emitters/nestjs/view.emitter.spec.ts | 217 + .../codegen/emitters/nestjs/view.emitter.ts | 121 + .../emitters/nestjs/worker.emitter.spec.ts | 270 + .../codegen/emitters/nestjs/worker.emitter.ts | 179 + .../src/codegen/import-resolver.service.ts | 100 + apps/server/src/codegen/imports.ts | 114 + apps/server/src/codegen/ir.spec.ts | 548 ++ apps/server/src/codegen/ir.ts | 1570 +++++ apps/server/src/codegen/naming.spec.ts | 155 + apps/server/src/codegen/naming.ts | 535 ++ .../src/codegen/openapi.emitter.spec.ts | 58 + apps/server/src/codegen/openapi.emitter.ts | 112 + .../src/codegen/simple-projection.spec.ts | 145 + apps/server/src/codegen/simple-projection.ts | 532 ++ .../src/codegen/surgical-fill.repository.ts | 61 + apps/server/src/codegen/surgical.spec.ts | 50 + apps/server/src/codegen/surgical.ts | 72 + apps/server/src/codegen/types.ts | 124 + apps/server/src/common/envelope.ts | 26 + .../exceptions/payment-required.exception.ts | 8 + .../src/common/filters/conflict.filter.ts | 26 + .../src/common/filters/forbidden.filter.ts | 13 + .../common/filters/internal.filter.spec.ts | 60 + .../src/common/filters/internal.filter.ts | 49 + .../src/common/filters/not-found.filter.ts | 13 + .../common/filters/payment-required.filter.ts | 14 + .../filters/schema-error.filter.spec.ts | 64 + .../src/common/filters/schema-error.filter.ts | 27 + .../src/common/filters/unauthorized.filter.ts | 13 + .../src/common/guards/user-throttler.guard.ts | 22 + .../common/pipes/zod-validation.pipe.spec.ts | 22 + .../src/common/pipes/zod-validation.pipe.ts | 11 + apps/server/src/config/env-check.ts | 46 + apps/server/src/config/env.spec.ts | 44 + apps/server/src/config/env.ts | 93 + .../src/edge-types/edge-types.controller.ts | 47 + .../src/edge-types/edge-types.module.ts | 11 + .../src/edge-types/edge-types.service.spec.ts | 35 + .../src/edge-types/edge-types.service.ts | 66 + apps/server/src/edge-types/registry.ts | 111 + apps/server/src/edges/dto/create-edge.dto.ts | 16 + .../server/src/edges/dto/edge-response.dto.ts | 30 + apps/server/src/edges/dto/update-edge.dto.ts | 15 + .../server/src/edges/dto/validate-edge.dto.ts | 15 + apps/server/src/edges/edges.controller.ts | 133 + apps/server/src/edges/edges.module.ts | 15 + apps/server/src/edges/edges.repository.ts | 165 + apps/server/src/edges/edges.service.ts | 290 + .../src/edges/schemas/edge.schema.spec.ts | 60 + apps/server/src/edges/schemas/edge.schema.ts | 52 + .../src/embeddings/embeddings.factory.ts | 21 + .../src/embeddings/embeddings.module.ts | 9 + .../src/embeddings/embeddings.service.spec.ts | 28 + .../src/embeddings/embeddings.service.ts | 45 + .../server/src/embeddings/embeddings.types.ts | 9 + .../src/graph/dto/apply-graph-response.dto.ts | 34 + apps/server/src/graph/dto/apply-graph.dto.ts | 46 + apps/server/src/graph/graph.controller.ts | 35 + apps/server/src/graph/graph.module.ts | 15 + apps/server/src/graph/graph.service.spec.ts | 275 + apps/server/src/graph/graph.service.ts | 390 ++ .../src/health/health.controller.spec.ts | 30 + apps/server/src/health/health.controller.ts | 63 + apps/server/src/main.ts | 182 + .../neo4j/migrations/001_constraints.cypher | 8 + .../src/neo4j/migrations/002_auth.cypher | 8 + .../migrations/002_tab_constraint.cypher | 1 + .../src/neo4j/migrations/003_billing.cypher | 6 + .../neo4j/migrations/004_node_version.cypher | 4 + .../src/neo4j/migrations/005_api_keys.cypher | 11 + .../migrations/006_graph_revision.cypher | 5 + .../neo4j/migrations/data/001-enrich-faz-a.ts | 97 + .../neo4j/migrations/data/002-enrich-faz-b.ts | 100 + .../neo4j/migrations/data/003-enrich-faz-c.ts | 81 + .../data/004-pattern-vector-index.ts | 32 + .../src/neo4j/migrations/data/005-tabs.ts | 50 + apps/server/src/neo4j/migrations/run.ts | 44 + apps/server/src/neo4j/neo4j.module.ts | 24 + apps/server/src/neo4j/neo4j.service.spec.ts | 40 + apps/server/src/neo4j/neo4j.service.ts | 76 + .../src/node-types/node-types.controller.ts | 48 + .../src/node-types/node-types.module.ts | 11 + .../src/node-types/node-types.service.spec.ts | 83 + .../src/node-types/node-types.service.ts | 78 + apps/server/src/node-types/registry.ts | 297 + apps/server/src/nodes/dto/create-node.dto.ts | 67 + .../server/src/nodes/dto/node-response.dto.ts | 5 + apps/server/src/nodes/dto/update-node.dto.ts | 18 + .../server/src/nodes/nodes.controller.spec.ts | 74 + apps/server/src/nodes/nodes.controller.ts | 108 + apps/server/src/nodes/nodes.module.ts | 14 + .../server/src/nodes/nodes.repository.spec.ts | 142 + apps/server/src/nodes/nodes.repository.ts | 186 + apps/server/src/nodes/nodes.service.spec.ts | 161 + apps/server/src/nodes/nodes.service.ts | 243 + .../nodes/schemas/api-gateway.schema.spec.ts | 56 + .../src/nodes/schemas/api-gateway.schema.ts | 24 + .../src/nodes/schemas/base.schema.spec.ts | 43 + apps/server/src/nodes/schemas/base.schema.ts | 27 + .../src/nodes/schemas/cache.schema.spec.ts | 45 + apps/server/src/nodes/schemas/cache.schema.ts | 18 + .../nodes/schemas/controller.schema.spec.ts | 88 + .../src/nodes/schemas/controller.schema.ts | 49 + .../src/nodes/schemas/dto.schema.spec.ts | 81 + apps/server/src/nodes/schemas/dto.schema.ts | 32 + .../src/nodes/schemas/enum.schema.spec.ts | 63 + apps/server/src/nodes/schemas/enum.schema.ts | 31 + .../nodes/schemas/env-variable.schema.spec.ts | 49 + .../src/nodes/schemas/env-variable.schema.ts | 18 + .../schemas/event-handler.schema.spec.ts | 52 + .../src/nodes/schemas/event-handler.schema.ts | 22 + .../nodes/schemas/exception.schema.spec.ts | 40 + .../src/nodes/schemas/exception.schema.ts | 16 + .../schemas/external-service.schema.spec.ts | 49 + .../nodes/schemas/external-service.schema.ts | 25 + .../nodes/schemas/frontend-app.schema.spec.ts | 51 + .../src/nodes/schemas/frontend-app.schema.ts | 22 + apps/server/src/nodes/schemas/index.spec.ts | 34 + apps/server/src/nodes/schemas/index.ts | 184 + .../schemas/message-queue.schema.spec.ts | 53 + .../src/nodes/schemas/message-queue.schema.ts | 19 + .../nodes/schemas/middleware.schema.spec.ts | 50 + .../src/nodes/schemas/middleware.schema.ts | 16 + .../src/nodes/schemas/model.schema.spec.ts | 90 + apps/server/src/nodes/schemas/model.schema.ts | 40 + .../src/nodes/schemas/module.schema.spec.ts | 47 + .../server/src/nodes/schemas/module.schema.ts | 15 + .../nodes/schemas/orchestrator.schema.spec.ts | 56 + .../src/nodes/schemas/orchestrator.schema.ts | 22 + .../nodes/schemas/repository.schema.spec.ts | 56 + .../src/nodes/schemas/repository.schema.ts | 24 + .../src/nodes/schemas/service.schema.spec.ts | 69 + .../src/nodes/schemas/service.schema.ts | 47 + .../src/nodes/schemas/table.schema.spec.ts | 137 + apps/server/src/nodes/schemas/table.schema.ts | 56 + .../nodes/schemas/ui-component.schema.spec.ts | 55 + .../src/nodes/schemas/ui-component.schema.ts | 32 + apps/server/src/nodes/schemas/version.spec.ts | 9 + apps/server/src/nodes/schemas/version.ts | 5 + .../src/nodes/schemas/view.schema.spec.ts | 60 + apps/server/src/nodes/schemas/view.schema.ts | 17 + .../src/nodes/schemas/worker.schema.spec.ts | 57 + .../server/src/nodes/schemas/worker.schema.ts | 24 + .../server/src/nodes/secret-redaction.spec.ts | 52 + apps/server/src/nodes/secret-redaction.ts | 40 + .../src/nodes/validate-properties.spec.ts | 54 + apps/server/src/nodes/validate-properties.ts | 33 + .../src/patterns/dto/create-pattern.dto.ts | 4 + .../src/patterns/dto/promote-pattern.dto.ts | 13 + .../src/patterns/dto/search-pattern.dto.ts | 4 + .../src/patterns/patterns.controller.spec.ts | 27 + .../src/patterns/patterns.controller.ts | 47 + apps/server/src/patterns/patterns.module.ts | 15 + .../src/patterns/patterns.repository.spec.ts | 36 + .../src/patterns/patterns.repository.ts | 107 + .../src/patterns/patterns.service.spec.ts | 61 + apps/server/src/patterns/patterns.service.ts | 111 + .../patterns/schemas/pattern.schema.spec.ts | 39 + .../src/patterns/schemas/pattern.schema.ts | 57 + .../src/patterns/seed/canonical-patterns.ts | 202 + apps/server/src/patterns/seed/seed.ts | 42 + .../src/projects/dto/create-project.dto.ts | 14 + .../src/projects/dto/project-response.dto.ts | 22 + .../projects/dto/report-implementation.dto.ts | 26 + .../src/projects/dto/update-project.dto.ts | 13 + .../src/projects/projects.controller.ts | 144 + apps/server/src/projects/projects.module.ts | 16 + .../src/projects/projects.repository.ts | 332 ++ .../src/projects/projects.service.spec.ts | 194 + apps/server/src/projects/projects.service.ts | 151 + .../projects/schemas/project.schema.spec.ts | 36 + .../src/projects/schemas/project.schema.ts | 18 + .../checkers/circular-dependency.checker.ts | 43 + .../rules/checkers/empty-schema.checker.ts | 24 + .../rules/checkers/type-mismatch.checker.ts | 49 + apps/server/src/rules/registry/blacklist.ts | 76 + apps/server/src/rules/registry/conditional.ts | 30 + apps/server/src/rules/registry/whitelist.ts | 103 + apps/server/src/rules/review.controller.ts | 48 + apps/server/src/rules/rules.controller.ts | 36 + apps/server/src/rules/rules.engine.spec.ts | 192 + apps/server/src/rules/rules.engine.ts | 207 + apps/server/src/rules/rules.module.ts | 14 + apps/server/src/rules/types.ts | 100 + apps/server/src/tabs/dto/create-tab.dto.ts | 4 + apps/server/src/tabs/dto/layout.dto.ts | 4 + apps/server/src/tabs/dto/reference.dto.ts | 4 + apps/server/src/tabs/dto/update-tab.dto.ts | 4 + .../src/tabs/schemas/tab.schema.spec.ts | 17 + apps/server/src/tabs/schemas/tab.schema.ts | 66 + apps/server/src/tabs/tabs.controller.spec.ts | 16 + apps/server/src/tabs/tabs.controller.ts | 85 + apps/server/src/tabs/tabs.module.ts | 13 + apps/server/src/tabs/tabs.repository.spec.ts | 27 + apps/server/src/tabs/tabs.repository.ts | 213 + apps/server/src/tabs/tabs.service.spec.ts | 51 + apps/server/src/tabs/tabs.service.ts | 108 + apps/server/src/types/polar-webhooks.d.ts | 6 + apps/server/src/value-sets/registry.ts | 271 + .../src/value-sets/value-sets.controller.ts | 35 + .../src/value-sets/value-sets.module.ts | 10 + .../src/value-sets/value-sets.service.ts | 35 + apps/server/test/auth.e2e-spec.ts | 129 + apps/server/test/billing.e2e-spec.ts | 140 + apps/server/test/codegen.e2e-spec.ts | 379 ++ apps/server/test/edges.e2e-spec.ts | 125 + apps/server/test/health.e2e-spec.ts | 86 + apps/server/test/nodes.e2e-spec.ts | 203 + apps/server/test/patterns.e2e-spec.ts | 122 + apps/server/test/tabs.e2e-spec.ts | 113 + apps/server/test/test-auth.ts | 39 + apps/server/tsconfig.build.json | 4 + apps/server/tsconfig.json | 29 + apps/server/vitest.config.ts | 25 + apps/server/vitest.e2e.config.ts | 39 + apps/server/vitest.gate.config.ts | 27 + apps/web/.env.example | 16 + apps/web/.gitignore | 37 + apps/web/Dockerfile | 31 + apps/web/README.md | 119 + apps/web/components.json | 21 + apps/web/eslint.config.js | 22 + apps/web/index.html | 31 + apps/web/package.json | 74 + apps/web/postcss.config.js | 6 + apps/web/public/favicon.svg | 9 + apps/web/public/fonts/Excalifont.woff2 | Bin 0 -> 24956 bytes apps/web/public/icons.svg | 24 + apps/web/public/logo.svg | 17 + apps/web/public/solarch.svg | 278 + apps/web/src/api/ai.ts | 357 ++ apps/web/src/api/api-keys.ts | 58 + apps/web/src/api/billing.ts | 92 + apps/web/src/api/client.ts | 90 + apps/web/src/api/codegen.ts | 427 ++ apps/web/src/api/edges.ts | 43 + apps/web/src/api/node-types.ts | 41 + apps/web/src/api/nodes.ts | 133 + apps/web/src/api/openapi.ts | 71 + apps/web/src/api/patterns.ts | 102 + apps/web/src/api/projects.ts | 43 + apps/web/src/api/raw.ts | 102 + apps/web/src/api/review.ts | 45 + apps/web/src/api/rules.ts | 77 + apps/web/src/api/schema.d.ts | 2642 +++++++++ apps/web/src/api/tabs.ts | 125 + apps/web/src/api/value-sets.ts | 60 + apps/web/src/app/AppShell.css | 22 + apps/web/src/app/AppShell.tsx | 150 + apps/web/src/app/GlobalErrorBoundary.tsx | 40 + apps/web/src/app/ThemeController.tsx | 63 + apps/web/src/app/providers.tsx | 166 + apps/web/src/app/route-guards.tsx | 62 + apps/web/src/app/router.tsx | 48 + apps/web/src/assets/hero.png | Bin 0 -> 13057 bytes apps/web/src/assets/react.svg | 1 + apps/web/src/assets/vite.svg | 1 + apps/web/src/canvas/CanvasA11yMirror.tsx | 89 + apps/web/src/canvas/CanvasView.css | 58 + apps/web/src/canvas/CanvasView.tsx | 1270 ++++ apps/web/src/canvas/NodeActionBar.tsx | 192 + apps/web/src/canvas/NodeHoverCard.tsx | 96 + apps/web/src/canvas/NodeNameEditor.tsx | 133 + apps/web/src/canvas/arrange.ts | 131 + apps/web/src/canvas/canvas-commands.ts | 68 + apps/web/src/canvas/coord-utils.ts | 36 + apps/web/src/canvas/edge-bundling.ts | 179 + apps/web/src/canvas/edge-hops.ts | 129 + apps/web/src/canvas/edge-router.ts | 103 + apps/web/src/canvas/edge-style.ts | 81 + apps/web/src/canvas/families.ts | 64 + apps/web/src/canvas/hover-preview.ts | 88 + apps/web/src/canvas/name-keys.ts | 31 + apps/web/src/canvas/node-templates.ts | 76 + apps/web/src/canvas/renderer.ts | 1924 ++++++ apps/web/src/canvas/types.ts | 68 + apps/web/src/components/BottomBar.tsx | 237 + apps/web/src/components/CommandPalette.tsx | 299 + apps/web/src/components/DocsModal.tsx | 573 ++ apps/web/src/components/EditorModal.tsx | 71 + .../src/components/Inspector/GenericForm.tsx | 30 + .../src/components/Inspector/Inspector.css | 125 + .../components/Inspector/InspectorPanel.tsx | 119 + .../src/components/Inspector/SchemaFields.tsx | 143 + .../src/components/Inspector/SchemaForm.tsx | 164 + .../Inspector/primitives/AddRowButton.tsx | 29 + .../primitives/ColumnMultiSelect.tsx | 58 + .../Inspector/primitives/DeleteButton.tsx | 17 + .../Inspector/primitives/DrawerShell.tsx | 122 + .../Inspector/primitives/DrawerTrigger.tsx | 73 + .../Inspector/primitives/EditGrid.tsx | 190 + .../Inspector/primitives/EmptyHint.tsx | 21 + .../Inspector/primitives/Eyebrow.tsx | 10 + .../components/Inspector/primitives/Field.tsx | 52 + .../Inspector/primitives/IconButton.tsx | 42 + .../components/Inspector/primitives/Input.tsx | 40 + .../Inspector/primitives/InspectorShell.tsx | 96 + .../Inspector/primitives/ListContainer.tsx | 29 + .../Inspector/primitives/ListRow.tsx | 72 + .../Inspector/primitives/MoveButtons.tsx | 23 + .../Inspector/primitives/NodeRefCombobox.tsx | 263 + .../Inspector/primitives/NodeRefList.tsx | 68 + .../components/Inspector/primitives/Pill.tsx | 77 + .../Inspector/primitives/SaveStatus.tsx | 32 + .../Inspector/primitives/SectionHeader.tsx | 31 + .../Inspector/primitives/Segmented.tsx | 66 + .../Inspector/primitives/Select.tsx | 59 + .../Inspector/primitives/SubPageShell.tsx | 140 + .../Inspector/primitives/Switch.tsx | 25 + .../Inspector/primitives/Textarea.tsx | 30 + .../Inspector/primitives/ToggleCell.tsx | 39 + .../Inspector/primitives/ValueSetCombobox.tsx | 169 + .../Inspector/primitives/ValueSetSelect.tsx | 44 + .../components/Inspector/primitives/index.ts | 33 + .../src/components/Inspector/schema-utils.ts | 210 + .../Inspector/types/ControllerDrawer.tsx | 349 ++ .../Inspector/types/ControllerInspector.tsx | 89 + .../components/Inspector/types/DTODrawer.tsx | 233 + .../Inspector/types/DTOInspector.tsx | 70 + .../Inspector/types/ServiceDrawer.tsx | 400 ++ .../Inspector/types/ServiceInspector.tsx | 83 + .../Inspector/types/TableDrawer.tsx | 69 + .../Inspector/types/TableInspector.tsx | 79 + .../types/table/CheckConstraintsEditor.tsx | 63 + .../Inspector/types/table/ColumnsEditor.tsx | 148 + .../types/table/ForeignKeysEditor.tsx | 119 + .../Inspector/types/table/IndexesEditor.tsx | 94 + .../types/table/UniqueConstraintsEditor.tsx | 58 + .../components/Inspector/types/table/types.ts | 91 + .../Inspector/widgets/ArrayWidget.tsx | 135 + .../Inspector/widgets/BooleanWidget.tsx | 21 + .../Inspector/widgets/EnumWidget.tsx | 68 + .../Inspector/widgets/NumberWidget.tsx | 60 + .../Inspector/widgets/ObjectWidget.tsx | 52 + .../Inspector/widgets/StringWidget.tsx | 111 + apps/web/src/components/TopBar.tsx | 658 +++ apps/web/src/components/ViewSwitch.tsx | 193 + apps/web/src/components/ui/button.tsx | 57 + apps/web/src/components/ui/collapsible.tsx | 11 + apps/web/src/components/ui/command.tsx | 153 + apps/web/src/components/ui/confirm-dialog.tsx | 142 + apps/web/src/components/ui/dialog.tsx | 120 + apps/web/src/components/ui/drawer.tsx | 116 + apps/web/src/components/ui/dropdown-menu.tsx | 199 + apps/web/src/components/ui/input.tsx | 22 + apps/web/src/components/ui/label.tsx | 26 + apps/web/src/components/ui/select.tsx | 157 + apps/web/src/components/ui/separator.tsx | 31 + apps/web/src/components/ui/switch.tsx | 29 + apps/web/src/components/ui/textarea.tsx | 22 + apps/web/src/components/ui/tooltip.tsx | 30 + apps/web/src/features/api/ApiClientPanel.tsx | 118 + apps/web/src/features/api/ApiDocsPanel.tsx | 140 + .../src/features/api/reference/ApiSidebar.tsx | 441 ++ .../src/features/api/reference/Markdown.tsx | 112 + .../features/api/reference/MethodBadge.tsx | 98 + .../features/api/reference/OperationView.tsx | 555 ++ .../src/features/api/reference/SchemaTree.tsx | 407 ++ .../api/reference/SolarchApiReference.tsx | 279 + .../features/api/reference/TryItConsole.tsx | 421 ++ .../web/src/features/api/reference/openapi.ts | 312 + .../src/features/api/reference/transport.ts | 28 + apps/web/src/features/auth/AuthShell.tsx | 95 + .../src/features/auth/ForgotPasswordPage.tsx | 271 + .../src/features/auth/GuestSignupModal.tsx | 95 + apps/web/src/features/auth/OAuthButtons.tsx | 136 + .../src/features/auth/PasswordStrength.tsx | 45 + apps/web/src/features/auth/SignInPage.tsx | 205 + apps/web/src/features/auth/SignUpPage.tsx | 385 ++ .../web/src/features/auth/SsoCallbackPage.tsx | 28 + apps/web/src/features/auth/auth-ui.tsx | 148 + apps/web/src/features/auth/clerk-error.ts | 25 + apps/web/src/features/auth/oauth-flow.ts | 12 + .../src/features/auth/password-strength.ts | 31 + apps/web/src/features/auth/sign-up-flow.ts | 57 + .../src/features/auth/useClaimGuestProject.ts | 68 + apps/web/src/features/billing/AsciiCardFx.tsx | 160 + apps/web/src/features/billing/BillingPage.tsx | 367 ++ apps/web/src/features/canvas/AddNodeMenu.tsx | 247 + apps/web/src/features/canvas/EdgePicker.tsx | 72 + .../src/features/canvas/InlineAiPrompt.tsx | 215 + apps/web/src/features/canvas/LockedAiBar.tsx | 280 + apps/web/src/features/canvas/OmniBar.tsx | 553 ++ .../web/src/features/canvas/ProblemsPanel.tsx | 141 + apps/web/src/features/canvas/ProjectPage.tsx | 451 ++ apps/web/src/features/canvas/ProposalBar.tsx | 157 + .../src/features/canvas/QuickConnectMenu.tsx | 112 + .../src/features/canvas/markdown-chips.tsx | 115 + apps/web/src/features/canvas/node-chip.tsx | 112 + .../web/src/features/codegen/ActivityFeed.tsx | 79 + apps/web/src/features/codegen/CodeEditor.tsx | 67 + apps/web/src/features/codegen/CodePreview.tsx | 54 + apps/web/src/features/codegen/CodeViewer.tsx | 193 + .../web/src/features/codegen/CodegenPanel.tsx | 345 ++ apps/web/src/features/codegen/EditorTabs.tsx | 122 + apps/web/src/features/codegen/FileIcon.tsx | 69 + apps/web/src/features/codegen/FileTree.tsx | 223 + apps/web/src/features/codegen/FillChat.tsx | 439 ++ .../src/features/codegen/MarkdownPreview.tsx | 45 + .../web/src/features/codegen/SimpleEditor.tsx | 111 + apps/web/src/features/codegen/StatusBar.tsx | 73 + .../src/features/codegen/SurgicalHandoff.tsx | 186 + .../web/src/features/codegen/SurgicalRail.tsx | 316 + .../src/features/codegen/codegen-commands.ts | 46 + apps/web/src/features/codegen/lib.ts | 286 + apps/web/src/features/codegen/theme.ts | 145 + .../src/features/codegen/useFillTypewriter.ts | 129 + apps/web/src/features/legal/LegalShell.tsx | 89 + apps/web/src/features/legal/PrivacyPage.tsx | 193 + apps/web/src/features/legal/RefundPage.tsx | 101 + apps/web/src/features/legal/TermsPage.tsx | 205 + .../features/onboarding/OnboardingTour.tsx | 457 ++ .../src/features/settings/SettingsPage.tsx | 162 + .../src/features/simple/CapabilityFlow.tsx | 241 + .../src/features/simple/CapabilityList.tsx | 117 + apps/web/src/features/simple/HolisticMap.tsx | 594 ++ apps/web/src/features/simple/SimpleView.tsx | 28 + apps/web/src/features/simple/SketchMap.tsx | 1035 ++++ apps/web/src/features/simple/SystemMap.tsx | 183 + apps/web/src/features/simple/fixture.ts | 103 + apps/web/src/features/simple/types.ts | 96 + .../features/welcome/PatternGallerySheet.tsx | 94 + .../src/features/welcome/PatternPreview.tsx | 142 + apps/web/src/features/welcome/Welcome.tsx | 242 + apps/web/src/hooks/useInspectorUpdate.ts | 129 + apps/web/src/hooks/useTouchMode.ts | 43 + apps/web/src/index.css | 361 ++ apps/web/src/lib/analytics.tsx | 69 + apps/web/src/lib/docs-content.ts | 782 +++ apps/web/src/lib/env.ts | 38 + apps/web/src/lib/field-icons.ts | 177 + apps/web/src/lib/guest.ts | 91 + apps/web/src/lib/haptics.ts | 62 + apps/web/src/lib/node-icons.tsx | 71 + apps/web/src/lib/translate-guard.ts | 34 + apps/web/src/lib/utils.ts | 7 + apps/web/src/lib/z-layers.ts | 22 + apps/web/src/main.tsx | 21 + apps/web/src/state/canvas-state.ts | 31 + apps/web/src/state/canvas-view-mode.ts | 27 + apps/web/src/state/history.ts | 51 + apps/web/src/state/pending-proposal.ts | 56 + apps/web/src/state/selection.ts | 49 + apps/web/src/state/theme.ts | 101 + apps/web/src/state/ui-prefs.ts | 19 + apps/web/src/state/workspace-view.ts | 42 + apps/web/tailwind.config.ts | 126 + apps/web/tsconfig.app.json | 30 + apps/web/tsconfig.json | 12 + apps/web/tsconfig.node.json | 24 + apps/web/vite.config.ts | 29 + 595 files changed, 90688 insertions(+) create mode 100644 apps/server/.env.example create mode 100644 apps/server/.gitignore create mode 100644 apps/server/Dockerfile create mode 100644 apps/server/README.md create mode 100644 apps/server/deploy/Caddyfile create mode 100644 apps/server/deploy/apache-app.solarch.dev-le-ssl.conf create mode 100644 apps/server/deploy/apache-app.solarch.dev.conf create mode 100644 apps/server/deploy/placeholder-index.html create mode 100644 apps/server/deploy/solarch-backend.service create mode 100644 apps/server/deploy/solarch-neo4j-backup.service create mode 100644 apps/server/deploy/solarch-neo4j-backup.timer create mode 100644 apps/server/docker-compose.yml create mode 100644 apps/server/docker-entrypoint.sh create mode 100644 apps/server/docs/ops/backup-restore.md create mode 100644 apps/server/docs/ops/deploy.md create mode 100644 apps/server/docs/plans/2026-05-21-node-types-implementation.md create mode 100644 apps/server/docs/plans/2026-05-22-node-enrichment-implementation.md create mode 100644 apps/server/docs/plans/2026-05-22-phase4-graphrag-implementation.md create mode 100644 apps/server/docs/plans/2026-05-22-tabs-contexts-implementation.md create mode 100644 apps/server/docs/specs/2026-05-21-node-types-design.md create mode 100644 apps/server/docs/specs/2026-05-22-node-enrichment-design.md create mode 100644 apps/server/docs/specs/2026-05-22-phase4-graphrag-design.md create mode 100644 apps/server/docs/specs/2026-05-22-tabs-contexts-design.md create mode 100644 apps/server/docs/specs/2026-06-12-graph-revision-and-cli-push.md create mode 100644 apps/server/docs/superpowers/plans/2026-06-29-api-docs.md create mode 100644 apps/server/docs/superpowers/plans/2026-06-29-native-api-reference-planA.md create mode 100644 apps/server/docs/superpowers/specs/2026-06-29-api-docs-test-design.md create mode 100644 apps/server/docs/superpowers/specs/2026-06-29-native-api-reference-design.md create mode 100644 apps/server/eslint.config.mjs create mode 100644 apps/server/nest-cli.json create mode 100644 apps/server/package.json create mode 100644 apps/server/scripts/neo4j-backup.env.example create mode 100755 apps/server/scripts/neo4j-backup.sh create mode 100755 apps/server/scripts/neo4j-restore.sh create mode 100644 apps/server/src/ai/ai-idempotency.store.spec.ts create mode 100644 apps/server/src/ai/ai-idempotency.store.ts create mode 100644 apps/server/src/ai/ai.controller.ts create mode 100644 apps/server/src/ai/ai.module.ts create mode 100644 apps/server/src/ai/ai.service.spec.ts create mode 100644 apps/server/src/ai/ai.service.ts create mode 100644 apps/server/src/ai/dto/chat-response.dto.ts create mode 100644 apps/server/src/ai/dto/chat.dto.ts create mode 100644 apps/server/src/ai/prompts/system-prompt.spec.ts create mode 100644 apps/server/src/ai/prompts/system-prompt.ts create mode 100644 apps/server/src/ai/providers/llm.factory.ts create mode 100644 apps/server/src/ai/tools/apply-architecture-graph.tool.ts create mode 100644 apps/server/src/ai/tools/create-edge.tool.ts create mode 100644 apps/server/src/ai/tools/create-node.tool.ts create mode 100644 apps/server/src/ai/tools/delete-edge.tool.ts create mode 100644 apps/server/src/ai/tools/delete-node.tool.ts create mode 100644 apps/server/src/ai/tools/get-node.tool.ts create mode 100644 apps/server/src/ai/tools/update-node.tool.ts create mode 100644 apps/server/src/app.module.ts create mode 100644 apps/server/src/auth/access.ts create mode 100644 apps/server/src/auth/api-keys/api-keys.controller.ts create mode 100644 apps/server/src/auth/api-keys/api-keys.repository.ts create mode 100644 apps/server/src/auth/api-keys/api-keys.service.ts create mode 100644 apps/server/src/auth/auth.module.ts create mode 100644 apps/server/src/auth/auth.types.ts create mode 100644 apps/server/src/auth/clerk-auth.guard.spec.ts create mode 100644 apps/server/src/auth/clerk-auth.guard.ts create mode 100644 apps/server/src/auth/current-auth.decorator.ts create mode 100644 apps/server/src/auth/guest-cleanup.service.ts create mode 100644 apps/server/src/auth/guest-token.spec.ts create mode 100644 apps/server/src/auth/guest-token.ts create mode 100644 apps/server/src/auth/guest.controller.ts create mode 100644 apps/server/src/auth/project-access.guard.ts create mode 100644 apps/server/src/auth/public.decorator.ts create mode 100644 apps/server/src/billing/billing.controller.ts create mode 100644 apps/server/src/billing/billing.module.ts create mode 100644 apps/server/src/billing/billing.service.spec.ts create mode 100644 apps/server/src/billing/billing.service.ts create mode 100644 apps/server/src/billing/entitlements.spec.ts create mode 100644 apps/server/src/billing/entitlements.ts create mode 100644 apps/server/src/billing/polar.client.spec.ts create mode 100644 apps/server/src/billing/polar.client.ts create mode 100644 apps/server/src/billing/subscription.repository.ts create mode 100644 apps/server/src/codegen/__fixtures__/load.ts create mode 100644 apps/server/src/codegen/__fixtures__/realistic-graph.json create mode 100644 apps/server/src/codegen/api-doc.spec.ts create mode 100644 apps/server/src/codegen/cardinality.ts create mode 100644 apps/server/src/codegen/codegen-assembly.spec.ts create mode 100644 apps/server/src/codegen/codegen-deps-warmup.service.ts create mode 100644 apps/server/src/codegen/codegen-fill-deps.ts create mode 100644 apps/server/src/codegen/codegen-fill.service.spec.ts create mode 100644 apps/server/src/codegen/codegen-fill.service.ts create mode 100644 apps/server/src/codegen/codegen-tsc.gate.test.ts create mode 100644 apps/server/src/codegen/codegen.controller.spec.ts create mode 100644 apps/server/src/codegen/codegen.controller.ts create mode 100644 apps/server/src/codegen/codegen.module.ts create mode 100644 apps/server/src/codegen/codegen.service.spec.ts create mode 100644 apps/server/src/codegen/codegen.service.ts create mode 100644 apps/server/src/codegen/codegen.version.ts create mode 100644 apps/server/src/codegen/contract-lint.spec.ts create mode 100644 apps/server/src/codegen/contract-lint.ts create mode 100644 apps/server/src/codegen/dto/codegen.dto.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/api-gateway.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/api-gateway.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/cache.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/cache.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/controller.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/controller.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/dto.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/dto.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/entity-synthesis.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/entity-synthesis.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/enum.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/enum.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/event-handler.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/event-handler.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/exception-synthesis.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/exception-synthesis.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/exception.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/exception.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/external-service.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/external-service.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/index.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/message-queue.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/message-queue.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/middleware.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/middleware.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/migration-runner.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/model.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/model.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/module.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/module.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/orchestrator.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/orchestrator.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/repository.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/repository.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/scaffold.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/scaffold.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/service-spec.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/service-spec.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/service.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/service.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/sql-type-map.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/sql-type-map.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/stub.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/stub.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/surgical-plan.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/surgical-plan.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/table.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/table.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/view.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/view.emitter.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/worker.emitter.spec.ts create mode 100644 apps/server/src/codegen/emitters/nestjs/worker.emitter.ts create mode 100644 apps/server/src/codegen/import-resolver.service.ts create mode 100644 apps/server/src/codegen/imports.ts create mode 100644 apps/server/src/codegen/ir.spec.ts create mode 100644 apps/server/src/codegen/ir.ts create mode 100644 apps/server/src/codegen/naming.spec.ts create mode 100644 apps/server/src/codegen/naming.ts create mode 100644 apps/server/src/codegen/openapi.emitter.spec.ts create mode 100644 apps/server/src/codegen/openapi.emitter.ts create mode 100644 apps/server/src/codegen/simple-projection.spec.ts create mode 100644 apps/server/src/codegen/simple-projection.ts create mode 100644 apps/server/src/codegen/surgical-fill.repository.ts create mode 100644 apps/server/src/codegen/surgical.spec.ts create mode 100644 apps/server/src/codegen/surgical.ts create mode 100644 apps/server/src/codegen/types.ts create mode 100644 apps/server/src/common/envelope.ts create mode 100644 apps/server/src/common/exceptions/payment-required.exception.ts create mode 100644 apps/server/src/common/filters/conflict.filter.ts create mode 100644 apps/server/src/common/filters/forbidden.filter.ts create mode 100644 apps/server/src/common/filters/internal.filter.spec.ts create mode 100644 apps/server/src/common/filters/internal.filter.ts create mode 100644 apps/server/src/common/filters/not-found.filter.ts create mode 100644 apps/server/src/common/filters/payment-required.filter.ts create mode 100644 apps/server/src/common/filters/schema-error.filter.spec.ts create mode 100644 apps/server/src/common/filters/schema-error.filter.ts create mode 100644 apps/server/src/common/filters/unauthorized.filter.ts create mode 100644 apps/server/src/common/guards/user-throttler.guard.ts create mode 100644 apps/server/src/common/pipes/zod-validation.pipe.spec.ts create mode 100644 apps/server/src/common/pipes/zod-validation.pipe.ts create mode 100644 apps/server/src/config/env-check.ts create mode 100644 apps/server/src/config/env.spec.ts create mode 100644 apps/server/src/config/env.ts create mode 100644 apps/server/src/edge-types/edge-types.controller.ts create mode 100644 apps/server/src/edge-types/edge-types.module.ts create mode 100644 apps/server/src/edge-types/edge-types.service.spec.ts create mode 100644 apps/server/src/edge-types/edge-types.service.ts create mode 100644 apps/server/src/edge-types/registry.ts create mode 100644 apps/server/src/edges/dto/create-edge.dto.ts create mode 100644 apps/server/src/edges/dto/edge-response.dto.ts create mode 100644 apps/server/src/edges/dto/update-edge.dto.ts create mode 100644 apps/server/src/edges/dto/validate-edge.dto.ts create mode 100644 apps/server/src/edges/edges.controller.ts create mode 100644 apps/server/src/edges/edges.module.ts create mode 100644 apps/server/src/edges/edges.repository.ts create mode 100644 apps/server/src/edges/edges.service.ts create mode 100644 apps/server/src/edges/schemas/edge.schema.spec.ts create mode 100644 apps/server/src/edges/schemas/edge.schema.ts create mode 100644 apps/server/src/embeddings/embeddings.factory.ts create mode 100644 apps/server/src/embeddings/embeddings.module.ts create mode 100644 apps/server/src/embeddings/embeddings.service.spec.ts create mode 100644 apps/server/src/embeddings/embeddings.service.ts create mode 100644 apps/server/src/embeddings/embeddings.types.ts create mode 100644 apps/server/src/graph/dto/apply-graph-response.dto.ts create mode 100644 apps/server/src/graph/dto/apply-graph.dto.ts create mode 100644 apps/server/src/graph/graph.controller.ts create mode 100644 apps/server/src/graph/graph.module.ts create mode 100644 apps/server/src/graph/graph.service.spec.ts create mode 100644 apps/server/src/graph/graph.service.ts create mode 100644 apps/server/src/health/health.controller.spec.ts create mode 100644 apps/server/src/health/health.controller.ts create mode 100644 apps/server/src/main.ts create mode 100644 apps/server/src/neo4j/migrations/001_constraints.cypher create mode 100644 apps/server/src/neo4j/migrations/002_auth.cypher create mode 100644 apps/server/src/neo4j/migrations/002_tab_constraint.cypher create mode 100644 apps/server/src/neo4j/migrations/003_billing.cypher create mode 100644 apps/server/src/neo4j/migrations/004_node_version.cypher create mode 100644 apps/server/src/neo4j/migrations/005_api_keys.cypher create mode 100644 apps/server/src/neo4j/migrations/006_graph_revision.cypher create mode 100644 apps/server/src/neo4j/migrations/data/001-enrich-faz-a.ts create mode 100644 apps/server/src/neo4j/migrations/data/002-enrich-faz-b.ts create mode 100644 apps/server/src/neo4j/migrations/data/003-enrich-faz-c.ts create mode 100644 apps/server/src/neo4j/migrations/data/004-pattern-vector-index.ts create mode 100644 apps/server/src/neo4j/migrations/data/005-tabs.ts create mode 100644 apps/server/src/neo4j/migrations/run.ts create mode 100644 apps/server/src/neo4j/neo4j.module.ts create mode 100644 apps/server/src/neo4j/neo4j.service.spec.ts create mode 100644 apps/server/src/neo4j/neo4j.service.ts create mode 100644 apps/server/src/node-types/node-types.controller.ts create mode 100644 apps/server/src/node-types/node-types.module.ts create mode 100644 apps/server/src/node-types/node-types.service.spec.ts create mode 100644 apps/server/src/node-types/node-types.service.ts create mode 100644 apps/server/src/node-types/registry.ts create mode 100644 apps/server/src/nodes/dto/create-node.dto.ts create mode 100644 apps/server/src/nodes/dto/node-response.dto.ts create mode 100644 apps/server/src/nodes/dto/update-node.dto.ts create mode 100644 apps/server/src/nodes/nodes.controller.spec.ts create mode 100644 apps/server/src/nodes/nodes.controller.ts create mode 100644 apps/server/src/nodes/nodes.module.ts create mode 100644 apps/server/src/nodes/nodes.repository.spec.ts create mode 100644 apps/server/src/nodes/nodes.repository.ts create mode 100644 apps/server/src/nodes/nodes.service.spec.ts create mode 100644 apps/server/src/nodes/nodes.service.ts create mode 100644 apps/server/src/nodes/schemas/api-gateway.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/api-gateway.schema.ts create mode 100644 apps/server/src/nodes/schemas/base.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/base.schema.ts create mode 100644 apps/server/src/nodes/schemas/cache.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/cache.schema.ts create mode 100644 apps/server/src/nodes/schemas/controller.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/controller.schema.ts create mode 100644 apps/server/src/nodes/schemas/dto.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/dto.schema.ts create mode 100644 apps/server/src/nodes/schemas/enum.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/enum.schema.ts create mode 100644 apps/server/src/nodes/schemas/env-variable.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/env-variable.schema.ts create mode 100644 apps/server/src/nodes/schemas/event-handler.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/event-handler.schema.ts create mode 100644 apps/server/src/nodes/schemas/exception.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/exception.schema.ts create mode 100644 apps/server/src/nodes/schemas/external-service.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/external-service.schema.ts create mode 100644 apps/server/src/nodes/schemas/frontend-app.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/frontend-app.schema.ts create mode 100644 apps/server/src/nodes/schemas/index.spec.ts create mode 100644 apps/server/src/nodes/schemas/index.ts create mode 100644 apps/server/src/nodes/schemas/message-queue.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/message-queue.schema.ts create mode 100644 apps/server/src/nodes/schemas/middleware.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/middleware.schema.ts create mode 100644 apps/server/src/nodes/schemas/model.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/model.schema.ts create mode 100644 apps/server/src/nodes/schemas/module.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/module.schema.ts create mode 100644 apps/server/src/nodes/schemas/orchestrator.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/orchestrator.schema.ts create mode 100644 apps/server/src/nodes/schemas/repository.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/repository.schema.ts create mode 100644 apps/server/src/nodes/schemas/service.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/service.schema.ts create mode 100644 apps/server/src/nodes/schemas/table.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/table.schema.ts create mode 100644 apps/server/src/nodes/schemas/ui-component.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/ui-component.schema.ts create mode 100644 apps/server/src/nodes/schemas/version.spec.ts create mode 100644 apps/server/src/nodes/schemas/version.ts create mode 100644 apps/server/src/nodes/schemas/view.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/view.schema.ts create mode 100644 apps/server/src/nodes/schemas/worker.schema.spec.ts create mode 100644 apps/server/src/nodes/schemas/worker.schema.ts create mode 100644 apps/server/src/nodes/secret-redaction.spec.ts create mode 100644 apps/server/src/nodes/secret-redaction.ts create mode 100644 apps/server/src/nodes/validate-properties.spec.ts create mode 100644 apps/server/src/nodes/validate-properties.ts create mode 100644 apps/server/src/patterns/dto/create-pattern.dto.ts create mode 100644 apps/server/src/patterns/dto/promote-pattern.dto.ts create mode 100644 apps/server/src/patterns/dto/search-pattern.dto.ts create mode 100644 apps/server/src/patterns/patterns.controller.spec.ts create mode 100644 apps/server/src/patterns/patterns.controller.ts create mode 100644 apps/server/src/patterns/patterns.module.ts create mode 100644 apps/server/src/patterns/patterns.repository.spec.ts create mode 100644 apps/server/src/patterns/patterns.repository.ts create mode 100644 apps/server/src/patterns/patterns.service.spec.ts create mode 100644 apps/server/src/patterns/patterns.service.ts create mode 100644 apps/server/src/patterns/schemas/pattern.schema.spec.ts create mode 100644 apps/server/src/patterns/schemas/pattern.schema.ts create mode 100644 apps/server/src/patterns/seed/canonical-patterns.ts create mode 100644 apps/server/src/patterns/seed/seed.ts create mode 100644 apps/server/src/projects/dto/create-project.dto.ts create mode 100644 apps/server/src/projects/dto/project-response.dto.ts create mode 100644 apps/server/src/projects/dto/report-implementation.dto.ts create mode 100644 apps/server/src/projects/dto/update-project.dto.ts create mode 100644 apps/server/src/projects/projects.controller.ts create mode 100644 apps/server/src/projects/projects.module.ts create mode 100644 apps/server/src/projects/projects.repository.ts create mode 100644 apps/server/src/projects/projects.service.spec.ts create mode 100644 apps/server/src/projects/projects.service.ts create mode 100644 apps/server/src/projects/schemas/project.schema.spec.ts create mode 100644 apps/server/src/projects/schemas/project.schema.ts create mode 100644 apps/server/src/rules/checkers/circular-dependency.checker.ts create mode 100644 apps/server/src/rules/checkers/empty-schema.checker.ts create mode 100644 apps/server/src/rules/checkers/type-mismatch.checker.ts create mode 100644 apps/server/src/rules/registry/blacklist.ts create mode 100644 apps/server/src/rules/registry/conditional.ts create mode 100644 apps/server/src/rules/registry/whitelist.ts create mode 100644 apps/server/src/rules/review.controller.ts create mode 100644 apps/server/src/rules/rules.controller.ts create mode 100644 apps/server/src/rules/rules.engine.spec.ts create mode 100644 apps/server/src/rules/rules.engine.ts create mode 100644 apps/server/src/rules/rules.module.ts create mode 100644 apps/server/src/rules/types.ts create mode 100644 apps/server/src/tabs/dto/create-tab.dto.ts create mode 100644 apps/server/src/tabs/dto/layout.dto.ts create mode 100644 apps/server/src/tabs/dto/reference.dto.ts create mode 100644 apps/server/src/tabs/dto/update-tab.dto.ts create mode 100644 apps/server/src/tabs/schemas/tab.schema.spec.ts create mode 100644 apps/server/src/tabs/schemas/tab.schema.ts create mode 100644 apps/server/src/tabs/tabs.controller.spec.ts create mode 100644 apps/server/src/tabs/tabs.controller.ts create mode 100644 apps/server/src/tabs/tabs.module.ts create mode 100644 apps/server/src/tabs/tabs.repository.spec.ts create mode 100644 apps/server/src/tabs/tabs.repository.ts create mode 100644 apps/server/src/tabs/tabs.service.spec.ts create mode 100644 apps/server/src/tabs/tabs.service.ts create mode 100644 apps/server/src/types/polar-webhooks.d.ts create mode 100644 apps/server/src/value-sets/registry.ts create mode 100644 apps/server/src/value-sets/value-sets.controller.ts create mode 100644 apps/server/src/value-sets/value-sets.module.ts create mode 100644 apps/server/src/value-sets/value-sets.service.ts create mode 100644 apps/server/test/auth.e2e-spec.ts create mode 100644 apps/server/test/billing.e2e-spec.ts create mode 100644 apps/server/test/codegen.e2e-spec.ts create mode 100644 apps/server/test/edges.e2e-spec.ts create mode 100644 apps/server/test/health.e2e-spec.ts create mode 100644 apps/server/test/nodes.e2e-spec.ts create mode 100644 apps/server/test/patterns.e2e-spec.ts create mode 100644 apps/server/test/tabs.e2e-spec.ts create mode 100644 apps/server/test/test-auth.ts create mode 100644 apps/server/tsconfig.build.json create mode 100644 apps/server/tsconfig.json create mode 100644 apps/server/vitest.config.ts create mode 100644 apps/server/vitest.e2e.config.ts create mode 100644 apps/server/vitest.gate.config.ts create mode 100644 apps/web/.env.example create mode 100644 apps/web/.gitignore create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/README.md create mode 100644 apps/web/components.json create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.js create mode 100644 apps/web/public/favicon.svg create mode 100644 apps/web/public/fonts/Excalifont.woff2 create mode 100644 apps/web/public/icons.svg create mode 100644 apps/web/public/logo.svg create mode 100644 apps/web/public/solarch.svg create mode 100644 apps/web/src/api/ai.ts create mode 100644 apps/web/src/api/api-keys.ts create mode 100644 apps/web/src/api/billing.ts create mode 100644 apps/web/src/api/client.ts create mode 100644 apps/web/src/api/codegen.ts create mode 100644 apps/web/src/api/edges.ts create mode 100644 apps/web/src/api/node-types.ts create mode 100644 apps/web/src/api/nodes.ts create mode 100644 apps/web/src/api/openapi.ts create mode 100644 apps/web/src/api/patterns.ts create mode 100644 apps/web/src/api/projects.ts create mode 100644 apps/web/src/api/raw.ts create mode 100644 apps/web/src/api/review.ts create mode 100644 apps/web/src/api/rules.ts create mode 100644 apps/web/src/api/schema.d.ts create mode 100644 apps/web/src/api/tabs.ts create mode 100644 apps/web/src/api/value-sets.ts create mode 100644 apps/web/src/app/AppShell.css create mode 100644 apps/web/src/app/AppShell.tsx create mode 100644 apps/web/src/app/GlobalErrorBoundary.tsx create mode 100644 apps/web/src/app/ThemeController.tsx create mode 100644 apps/web/src/app/providers.tsx create mode 100644 apps/web/src/app/route-guards.tsx create mode 100644 apps/web/src/app/router.tsx create mode 100644 apps/web/src/assets/hero.png create mode 100644 apps/web/src/assets/react.svg create mode 100644 apps/web/src/assets/vite.svg create mode 100644 apps/web/src/canvas/CanvasA11yMirror.tsx create mode 100644 apps/web/src/canvas/CanvasView.css create mode 100644 apps/web/src/canvas/CanvasView.tsx create mode 100644 apps/web/src/canvas/NodeActionBar.tsx create mode 100644 apps/web/src/canvas/NodeHoverCard.tsx create mode 100644 apps/web/src/canvas/NodeNameEditor.tsx create mode 100644 apps/web/src/canvas/arrange.ts create mode 100644 apps/web/src/canvas/canvas-commands.ts create mode 100644 apps/web/src/canvas/coord-utils.ts create mode 100644 apps/web/src/canvas/edge-bundling.ts create mode 100644 apps/web/src/canvas/edge-hops.ts create mode 100644 apps/web/src/canvas/edge-router.ts create mode 100644 apps/web/src/canvas/edge-style.ts create mode 100644 apps/web/src/canvas/families.ts create mode 100644 apps/web/src/canvas/hover-preview.ts create mode 100644 apps/web/src/canvas/name-keys.ts create mode 100644 apps/web/src/canvas/node-templates.ts create mode 100644 apps/web/src/canvas/renderer.ts create mode 100644 apps/web/src/canvas/types.ts create mode 100644 apps/web/src/components/BottomBar.tsx create mode 100644 apps/web/src/components/CommandPalette.tsx create mode 100644 apps/web/src/components/DocsModal.tsx create mode 100644 apps/web/src/components/EditorModal.tsx create mode 100644 apps/web/src/components/Inspector/GenericForm.tsx create mode 100644 apps/web/src/components/Inspector/Inspector.css create mode 100644 apps/web/src/components/Inspector/InspectorPanel.tsx create mode 100644 apps/web/src/components/Inspector/SchemaFields.tsx create mode 100644 apps/web/src/components/Inspector/SchemaForm.tsx create mode 100644 apps/web/src/components/Inspector/primitives/AddRowButton.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ColumnMultiSelect.tsx create mode 100644 apps/web/src/components/Inspector/primitives/DeleteButton.tsx create mode 100644 apps/web/src/components/Inspector/primitives/DrawerShell.tsx create mode 100644 apps/web/src/components/Inspector/primitives/DrawerTrigger.tsx create mode 100644 apps/web/src/components/Inspector/primitives/EditGrid.tsx create mode 100644 apps/web/src/components/Inspector/primitives/EmptyHint.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Eyebrow.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Field.tsx create mode 100644 apps/web/src/components/Inspector/primitives/IconButton.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Input.tsx create mode 100644 apps/web/src/components/Inspector/primitives/InspectorShell.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ListContainer.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ListRow.tsx create mode 100644 apps/web/src/components/Inspector/primitives/MoveButtons.tsx create mode 100644 apps/web/src/components/Inspector/primitives/NodeRefCombobox.tsx create mode 100644 apps/web/src/components/Inspector/primitives/NodeRefList.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Pill.tsx create mode 100644 apps/web/src/components/Inspector/primitives/SaveStatus.tsx create mode 100644 apps/web/src/components/Inspector/primitives/SectionHeader.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Segmented.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Select.tsx create mode 100644 apps/web/src/components/Inspector/primitives/SubPageShell.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Switch.tsx create mode 100644 apps/web/src/components/Inspector/primitives/Textarea.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ToggleCell.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ValueSetCombobox.tsx create mode 100644 apps/web/src/components/Inspector/primitives/ValueSetSelect.tsx create mode 100644 apps/web/src/components/Inspector/primitives/index.ts create mode 100644 apps/web/src/components/Inspector/schema-utils.ts create mode 100644 apps/web/src/components/Inspector/types/ControllerDrawer.tsx create mode 100644 apps/web/src/components/Inspector/types/ControllerInspector.tsx create mode 100644 apps/web/src/components/Inspector/types/DTODrawer.tsx create mode 100644 apps/web/src/components/Inspector/types/DTOInspector.tsx create mode 100644 apps/web/src/components/Inspector/types/ServiceDrawer.tsx create mode 100644 apps/web/src/components/Inspector/types/ServiceInspector.tsx create mode 100644 apps/web/src/components/Inspector/types/TableDrawer.tsx create mode 100644 apps/web/src/components/Inspector/types/TableInspector.tsx create mode 100644 apps/web/src/components/Inspector/types/table/CheckConstraintsEditor.tsx create mode 100644 apps/web/src/components/Inspector/types/table/ColumnsEditor.tsx create mode 100644 apps/web/src/components/Inspector/types/table/ForeignKeysEditor.tsx create mode 100644 apps/web/src/components/Inspector/types/table/IndexesEditor.tsx create mode 100644 apps/web/src/components/Inspector/types/table/UniqueConstraintsEditor.tsx create mode 100644 apps/web/src/components/Inspector/types/table/types.ts create mode 100644 apps/web/src/components/Inspector/widgets/ArrayWidget.tsx create mode 100644 apps/web/src/components/Inspector/widgets/BooleanWidget.tsx create mode 100644 apps/web/src/components/Inspector/widgets/EnumWidget.tsx create mode 100644 apps/web/src/components/Inspector/widgets/NumberWidget.tsx create mode 100644 apps/web/src/components/Inspector/widgets/ObjectWidget.tsx create mode 100644 apps/web/src/components/Inspector/widgets/StringWidget.tsx create mode 100644 apps/web/src/components/TopBar.tsx create mode 100644 apps/web/src/components/ViewSwitch.tsx create mode 100644 apps/web/src/components/ui/button.tsx create mode 100644 apps/web/src/components/ui/collapsible.tsx create mode 100644 apps/web/src/components/ui/command.tsx create mode 100644 apps/web/src/components/ui/confirm-dialog.tsx create mode 100644 apps/web/src/components/ui/dialog.tsx create mode 100644 apps/web/src/components/ui/drawer.tsx create mode 100644 apps/web/src/components/ui/dropdown-menu.tsx create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/components/ui/label.tsx create mode 100644 apps/web/src/components/ui/select.tsx create mode 100644 apps/web/src/components/ui/separator.tsx create mode 100644 apps/web/src/components/ui/switch.tsx create mode 100644 apps/web/src/components/ui/textarea.tsx create mode 100644 apps/web/src/components/ui/tooltip.tsx create mode 100644 apps/web/src/features/api/ApiClientPanel.tsx create mode 100644 apps/web/src/features/api/ApiDocsPanel.tsx create mode 100644 apps/web/src/features/api/reference/ApiSidebar.tsx create mode 100644 apps/web/src/features/api/reference/Markdown.tsx create mode 100644 apps/web/src/features/api/reference/MethodBadge.tsx create mode 100644 apps/web/src/features/api/reference/OperationView.tsx create mode 100644 apps/web/src/features/api/reference/SchemaTree.tsx create mode 100644 apps/web/src/features/api/reference/SolarchApiReference.tsx create mode 100644 apps/web/src/features/api/reference/TryItConsole.tsx create mode 100644 apps/web/src/features/api/reference/openapi.ts create mode 100644 apps/web/src/features/api/reference/transport.ts create mode 100644 apps/web/src/features/auth/AuthShell.tsx create mode 100644 apps/web/src/features/auth/ForgotPasswordPage.tsx create mode 100644 apps/web/src/features/auth/GuestSignupModal.tsx create mode 100644 apps/web/src/features/auth/OAuthButtons.tsx create mode 100644 apps/web/src/features/auth/PasswordStrength.tsx create mode 100644 apps/web/src/features/auth/SignInPage.tsx create mode 100644 apps/web/src/features/auth/SignUpPage.tsx create mode 100644 apps/web/src/features/auth/SsoCallbackPage.tsx create mode 100644 apps/web/src/features/auth/auth-ui.tsx create mode 100644 apps/web/src/features/auth/clerk-error.ts create mode 100644 apps/web/src/features/auth/oauth-flow.ts create mode 100644 apps/web/src/features/auth/password-strength.ts create mode 100644 apps/web/src/features/auth/sign-up-flow.ts create mode 100644 apps/web/src/features/auth/useClaimGuestProject.ts create mode 100644 apps/web/src/features/billing/AsciiCardFx.tsx create mode 100644 apps/web/src/features/billing/BillingPage.tsx create mode 100644 apps/web/src/features/canvas/AddNodeMenu.tsx create mode 100644 apps/web/src/features/canvas/EdgePicker.tsx create mode 100644 apps/web/src/features/canvas/InlineAiPrompt.tsx create mode 100644 apps/web/src/features/canvas/LockedAiBar.tsx create mode 100644 apps/web/src/features/canvas/OmniBar.tsx create mode 100644 apps/web/src/features/canvas/ProblemsPanel.tsx create mode 100644 apps/web/src/features/canvas/ProjectPage.tsx create mode 100644 apps/web/src/features/canvas/ProposalBar.tsx create mode 100644 apps/web/src/features/canvas/QuickConnectMenu.tsx create mode 100644 apps/web/src/features/canvas/markdown-chips.tsx create mode 100644 apps/web/src/features/canvas/node-chip.tsx create mode 100644 apps/web/src/features/codegen/ActivityFeed.tsx create mode 100644 apps/web/src/features/codegen/CodeEditor.tsx create mode 100644 apps/web/src/features/codegen/CodePreview.tsx create mode 100644 apps/web/src/features/codegen/CodeViewer.tsx create mode 100644 apps/web/src/features/codegen/CodegenPanel.tsx create mode 100644 apps/web/src/features/codegen/EditorTabs.tsx create mode 100644 apps/web/src/features/codegen/FileIcon.tsx create mode 100644 apps/web/src/features/codegen/FileTree.tsx create mode 100644 apps/web/src/features/codegen/FillChat.tsx create mode 100644 apps/web/src/features/codegen/MarkdownPreview.tsx create mode 100644 apps/web/src/features/codegen/SimpleEditor.tsx create mode 100644 apps/web/src/features/codegen/StatusBar.tsx create mode 100644 apps/web/src/features/codegen/SurgicalHandoff.tsx create mode 100644 apps/web/src/features/codegen/SurgicalRail.tsx create mode 100644 apps/web/src/features/codegen/codegen-commands.ts create mode 100644 apps/web/src/features/codegen/lib.ts create mode 100644 apps/web/src/features/codegen/theme.ts create mode 100644 apps/web/src/features/codegen/useFillTypewriter.ts create mode 100644 apps/web/src/features/legal/LegalShell.tsx create mode 100644 apps/web/src/features/legal/PrivacyPage.tsx create mode 100644 apps/web/src/features/legal/RefundPage.tsx create mode 100644 apps/web/src/features/legal/TermsPage.tsx create mode 100644 apps/web/src/features/onboarding/OnboardingTour.tsx create mode 100644 apps/web/src/features/settings/SettingsPage.tsx create mode 100644 apps/web/src/features/simple/CapabilityFlow.tsx create mode 100644 apps/web/src/features/simple/CapabilityList.tsx create mode 100644 apps/web/src/features/simple/HolisticMap.tsx create mode 100644 apps/web/src/features/simple/SimpleView.tsx create mode 100644 apps/web/src/features/simple/SketchMap.tsx create mode 100644 apps/web/src/features/simple/SystemMap.tsx create mode 100644 apps/web/src/features/simple/fixture.ts create mode 100644 apps/web/src/features/simple/types.ts create mode 100644 apps/web/src/features/welcome/PatternGallerySheet.tsx create mode 100644 apps/web/src/features/welcome/PatternPreview.tsx create mode 100644 apps/web/src/features/welcome/Welcome.tsx create mode 100644 apps/web/src/hooks/useInspectorUpdate.ts create mode 100644 apps/web/src/hooks/useTouchMode.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/lib/analytics.tsx create mode 100644 apps/web/src/lib/docs-content.ts create mode 100644 apps/web/src/lib/env.ts create mode 100644 apps/web/src/lib/field-icons.ts create mode 100644 apps/web/src/lib/guest.ts create mode 100644 apps/web/src/lib/haptics.ts create mode 100644 apps/web/src/lib/node-icons.tsx create mode 100644 apps/web/src/lib/translate-guard.ts create mode 100644 apps/web/src/lib/utils.ts create mode 100644 apps/web/src/lib/z-layers.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/state/canvas-state.ts create mode 100644 apps/web/src/state/canvas-view-mode.ts create mode 100644 apps/web/src/state/history.ts create mode 100644 apps/web/src/state/pending-proposal.ts create mode 100644 apps/web/src/state/selection.ts create mode 100644 apps/web/src/state/theme.ts create mode 100644 apps/web/src/state/ui-prefs.ts create mode 100644 apps/web/src/state/workspace-view.ts create mode 100644 apps/web/tailwind.config.ts create mode 100644 apps/web/tsconfig.app.json create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts diff --git a/apps/server/.env.example b/apps/server/.env.example new file mode 100644 index 0000000..0143b3f --- /dev/null +++ b/apps/server/.env.example @@ -0,0 +1,41 @@ +NODE_ENV=development +PORT=4000 + +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=solarch_dev_password + +CORS_ORIGIN=http://localhost:3000 + +# ── Auth (Clerk) — required (sign-in is disabled when blank) ────────── +# https://clerk.com → create an app, enable Organizations, copy the keys. +CLERK_SECRET_KEY=sk_test_xxxxxxxx +CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxx +# Optional CSRF protection — comma-separated allowed origins (e.g. http://localhost:5173) +# CLERK_AUTHORIZED_PARTIES=http://localhost:5173 + +# ── Guest mode (one project without signing in) ────────────────────── +# Secret that signs guest tickets: openssl rand -hex 32. Blank = guest mode off. +GUEST_TOKEN_SECRET= + +# ── Billing (Polar — Merchant of Record) — sandbox; get IDs from the Polar dashboard ── +POLAR_SERVER=sandbox +POLAR_ACCESS_TOKEN=polar_oat_xxx +POLAR_WEBHOOK_SECRET=whsec_xxx +POLAR_PRODUCT_DRAW=prod_xxx +POLAR_PRODUCT_BUILD=prod_xxx +POLAR_PRODUCT_CODE=prod_xxx + +# ── AI agent — optional (any OpenAI-compatible provider) ────────────── +# generation = architecture generation + tool calling, chat = general dialogue +LLM_GENERATION_PROVIDER=deepseek +LLM_CHAT_PROVIDER=deepseek +# Bedrock (OpenAI-compatible endpoint) + long-term bearer API key +# BEDROCK_API_KEY=ABSK-xxxxxxxx +# BEDROCK_BASE_URL=https://bedrock-mantle.us-east-1.api.aws/v1 +# AWS_REGION=us-east-1 +# BEDROCK_MODEL=moonshotai.kimi-k2.5 +# DeepSeek (proven tool calling) +DEEPSEEK_API_KEY=sk-xxxxxxxx +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-v4-pro diff --git a/apps/server/.gitignore b/apps/server/.gitignore new file mode 100644 index 0000000..01f2959 --- /dev/null +++ b/apps/server/.gitignore @@ -0,0 +1,47 @@ +# Dependencies +node_modules/ +.pnpm-store/ + +# Build output +dist/ +build/ +*.tsbuildinfo + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Runtime +.cache/ +.tmp/ + +# Environment +.env +.env.local +.env.*.local + +# Editor / OS +.vscode/ +.idea/ +.DS_Store +Thumbs.db +*.swp +*.swo + +# Test / coverage +coverage/ +.nyc_output/ + +# Neo4j local data +neo4j_data/ + +# DB yedekleri (PII içerir — repo'ya girmesin) + yedek ayar dosyası (RCLONE token vb.) +backups/ +scripts/neo4j-backup.env + +# Claude Code +.claude/ diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..d8ffb41 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,35 @@ +# syntax=docker/dockerfile:1 +# Solarch server (NestJS + Neo4j). Build context = repo root (pnpm workspace). + +FROM node:22-slim AS base +RUN corepack enable && corepack prepare pnpm@10.0.0 --activate +WORKDIR /app + +# ---- install + build ---- +FROM base AS build +# Workspace manifests first for a cached install layer. +COPY pnpm-workspace.yaml package.json pnpm-lock.yaml turbo.json tsconfig.base.json ./ +COPY apps/web/package.json apps/web/package.json +COPY apps/server/package.json apps/server/package.json +RUN pnpm install --frozen-lockfile --filter @solarch/server... +COPY apps/server apps/server +RUN pnpm --filter @solarch/server build +# Pre-fetch the local embedding model so the first request is offline-ready (best-effort: +# a restricted build network falls back to downloading it on first use at runtime). +RUN cd apps/server && node -e "import('@xenova/transformers').then(m=>m.pipeline('feature-extraction','Xenova/paraphrase-multilingual-MiniLM-L12-v2')).then(()=>console.log('embed model cached')).catch(e=>console.error('embed prefetch skipped:',e.message))" || true + +# ---- runtime ---- +FROM base AS runtime +ENV NODE_ENV=production +WORKDIR /app +# tsx + src are kept so the entrypoint can run the (idempotent) DB bootstrap. +COPY --from=build /app/node_modules ./node_modules +# tsconfig.base.json is required at runtime: apps/server/tsconfig.json extends it, and the +# entrypoint runs the DB bootstrap via tsx (which reads the tsconfig for experimentalDecorators). +COPY --from=build /app/package.json /app/pnpm-workspace.yaml /app/turbo.json /app/tsconfig.base.json ./ +COPY --from=build /app/apps/server ./apps/server +COPY apps/server/docker-entrypoint.sh /usr/local/bin/solarch-entrypoint.sh +RUN chmod +x /usr/local/bin/solarch-entrypoint.sh +WORKDIR /app/apps/server +EXPOSE 4000 +ENTRYPOINT ["/usr/local/bin/solarch-entrypoint.sh"] diff --git a/apps/server/README.md b/apps/server/README.md new file mode 100644 index 0000000..68cfa5a --- /dev/null +++ b/apps/server/README.md @@ -0,0 +1,154 @@ +# solarch-backend + +Solarch'ın mimari graf backend'i — **kural-güdümlü ve AI-augmented**. Node + Edge CRUD, Rules Engine, GraphRAG ve doğal-dil mimari üretimi tek serviste birleşir. Frontend'ten bağımsız geliştirilir; sözleşme `/api/v1/*` üzerinden tipli (Zod + OpenAPI/Scalar) yürür. + +## Stack + +- **NestJS 11** — modüler yapı, DI, global ZodValidationPipe, 4 ExceptionFilter, `ok()` envelope +- **Neo4j 5** (community) — graf veritabanı + **native vector index** (ekstra DB yok) +- **Zod 4 + nestjs-zod** — discriminated union, runtime + tip + OpenAPI üç başlı şema +- **@langchain/deepseek** — `ChatDeepSeek` v4-flash, `response_format: json_object` +- **@xenova/transformers** — lokal MiniLM-L12-v2 (Türkçe dahil 50+ dil, 384-d) +- **TypeScript 6 · pnpm 10 · Vitest 2 · Testcontainers** — SWC vitest, ManagedTransaction +- **Scalar** — `/docs` üzerinden interaktif API explorer (Swagger değil) + +## Modüller + +| Modül | Sorumluluk | +|---|---| +| `auth` | Clerk JWT + misafir bileti + **API anahtarı** (`slk_…`, SHA-256 hash'li) — üç kimlik yolu tek guard'da | +| `projects` | Project CRUD + `getGraph(projectId)` + **`graphRevision`** sayacı | +| `tabs` | Çoklu sekme / context — **tek-ev + referans** modeli (`homeTabId` + `REFERENCES {x,y}`) | +| `nodes` | 21 node tipi CRUD, Zod discriminated union, kind başına şema, `version` + `expectedVersion` optimistic locking | +| `edges` | 16 edge kind CRUD + paylaşımlı `properties` şeması | +| `graph` | Batch `apply()` — AI, UI ve **CLI push**'tan gelen mutasyonları atomik commit; mevcut node'lara edge bağlayabilen upsert + `baseRevision` çatışma kontrolü | +| `rules` | Rules Engine: 32 whitelist · 7 blacklist (ERR_001..007) · 3 conditional | +| `node-types` / `edge-types` | Katalog endpoint'leri + `fieldHints` (UI inspector için) | +| `patterns` | Kanonik desen kütüphanesi (12 seed) — GraphRAG kaynağı | +| `embeddings` | Lokal MiniLM, Neo4j vector index — cosine similarity | +| `ai` | Doğal dilden mimari üretim — GraphRAG → structured output → self-correction | + +## Veri modeli (özet) + +``` +(:Project)─HAS─→(:Tab) +(:Project)─HAS─→(:Node)─owns─(:Tab via homeTabId) +(:Tab)─REFERENCES {x,y}─→(:Node) # import / kısayol +(:Node)─[EDGE_KIND {projectId,...}]─→(:Node) +(:Project)─HAS─→(:Pattern {embedding[]}) # global kütüphane +``` + +**Tek-ev + referans:** Her node tek bir tab'da yaşar (`positionX/Y` orada). Başka tab'lara `(:Tab)-[:REFERENCES {x,y}]->(:Node)` ile **import** edilir — kopya değil, tek kaynak. Rules Engine sekmeden bağımsız mantıksal graf üzerinde çalışır. + +## Eşzamanlılık: iki katmanlı çatışma koruması + +| Katman | Mekanizma | Çatışmada | +|---|---|---| +| **Graf revizyonu** | `Project.graphRevision` — her yapısal mutasyonda +1 (node/edge create-update-delete, `graph/apply`). Pozisyon/tab layout kaydetme bump **etmez**. | `graph/apply` + `baseRevision` eskiyse hiçbir şey yazılmadan `409 ERR_GRAPH_REVISION_CONFLICT` + `currentRevision` | +| **Node versiyonu** | `Node.version` — her başarılı PATCH +1; istemci `expectedVersion` gönderir. | `409 ERR_VERSION_CONFLICT` + `currentVersion` | + +`GET /projects/:id/graph` yanıtı `graphRevision` döner; CLI push delta'yı bu +revizyona göre hesaplar ve 409'da otomatik re-pull + tek retry yapar. Tasarım +detayı: [`docs/specs/2026-06-12-graph-revision-and-cli-push.md`](docs/specs/2026-06-12-graph-revision-and-cli-push.md). + +## Kimlik doğrulama (üç yol, tek guard) + +`ClerkAuthGuard` sırayla dener: + +1. **Clerk JWT** (web uygulaması — `Authorization: Bearer `) +2. **Misafir bileti** (`X-Guest-Token` header'ı veya `solarch_guest_token` çerezi — HMAC imzalı, 30 gün TTL) +3. **API anahtarı** (CLI/MCP — `Authorization: Bearer slk_…`; Neo4j'de yalnız SHA-256 hash saklanır, anahtar bir kez gösterilir) + +Anahtar yönetimi: `POST/GET/DELETE /api/v1/api-keys` (Clerk oturumu gerekir, misafir açamaz; kullanıcı başına en çok 10 anahtar). + +## AI akışı + +Doğal dil → embed → Neo4j vector search (top-K pattern) → system prompt + RAG context → DeepSeek v4-flash (`json_object`) → Zod parse → `graph.apply()` → Rules Engine değerlendirir → ihlal varsa **self-correction loop** (max 5 deneme) → atomik commit. + +Detay: [`docs/architecture/ai-flow.md`](docs/) + repo kök `SOLARCH-ARCHITECTURE.md` (üst dizin). + +## API yüzeyi (öne çıkan) + +| Method | Path | Açıklama | +|---|---|---| +| `GET` | `/api/v1/health` | Liveness | +| `GET/POST` | `/api/v1/projects` | Project CRUD | +| `GET` | `/api/v1/projects/:pid/graph` | Tüm graf + `graphRevision` | +| `PUT` | `/api/v1/projects/:pid/implementation` | İmplementasyon sayaçları (`implTotal`, `implFilled`, `implAi`) — CLI/eklenti raporu; yapısal mutasyon değil | +| `GET/POST/DELETE` | `/api/v1/projects/:pid/tabs[/:tabId]` | Tab CRUD | +| `GET` | `/api/v1/projects/:pid/tabs/:tabId/graph` | Tab-scoped graf (home + REFERENCES) | +| `POST` | `/api/v1/projects/:pid/tabs/:tabId/references` | Node'u tab'a import | +| `*` | `/api/v1/projects/:pid/nodes[/:nodeId]` | Node CRUD (21 tip) | +| `*` | `/api/v1/projects/:pid/edges[/:edgeId]` | Edge CRUD (16 kind) | +| `POST` | `/api/v1/projects/:pid/graph/apply` | Atomik batch mutate (AI + UI + CLI push) — tempId/cloudId karışık edge uçları, opsiyonel `baseRevision` | +| `POST/GET/DELETE` | `/api/v1/api-keys[/:keyId]` | API anahtarı yönetimi (CLI/MCP kimliği) | +| `POST` | `/api/v1/projects/:pid/projects/:id/repair` | Tek-round repair | +| `GET` | `/api/v1/node-types` · `/edge-types` · `/rules` | Katalog | +| `POST` | `/api/v1/projects/:pid/ai/chat` | Doğal dil mimari üretim | +| `GET` | `/docs` | Scalar UI | + +## Geliştirme + +```bash +# 1. Bağımlılıklar +pnpm install + +# 2. Neo4j (Docker) +pnpm neo4j:up + +# 3. Constraints + vector index migration +cp .env.example .env # gerekli anahtarları doldur +pnpm neo4j:migrate + +# 4. Dev server (watch + Scalar at /docs) +pnpm dev # http://localhost:4000/api/v1 + +# 5. Testler +pnpm test # unit +pnpm test:e2e # e2e (Testcontainers — ilk run ~2dk) +``` + +## Çevre değişkenleri (özet) + +| Anahtar | Default | Not | +|---|---|---| +| `PORT` | `4000` | | +| `NEO4J_URI` / `USER` / `PASSWORD` | — | zorunlu | +| `CORS_ORIGIN` | `http://localhost:3000` | frontend origin'i | +| `LLM_GENERATION_PROVIDER` | `deepseek` | `bedrock` da var | +| `DEEPSEEK_API_KEY` | — | yoksa `/ai/chat` 503 | +| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | compose tier; v4-pro çok yavaş | +| `EMBED_PROVIDER` | `local` | `@xenova/transformers` (offline) | +| `EMBED_MODEL` | `Xenova/paraphrase-multilingual-MiniLM-L12-v2` | 384-d, çok dilli | +| `EMBED_TOP_K` / `MIN_SCORE` | `3` / `0.7` | GraphRAG eşikleri | + +Tam liste: [`src/config/env.ts`](src/config/env.ts). + +## Yol haritası + +| Faz | Kapsam | Durum | +|---|---|---| +| 1 | Project + Node CRUD (Veri ailesi) | DONE | +| 1.5 | Diğer node aileleri (21 tip toplam) | DONE | +| 2 | Edge CRUD + Rules Engine (whitelist/blacklist/conditional) | DONE | +| 3 | Graph atomik apply + Tabs/Contexts (tek-ev + referans) | DONE | +| 3B | AI Service — GraphRAG + structured output + self-correction | DONE | +| 4 | Node enrichment — codegen-ready properties (Faz A/B/C) | DONE | +| 5 | Kod üretim motoru (AST scaffold + cerrahi AI) | DONE | +| 2.0-1 | API anahtarları (CLI/MCP kimliği) — SOLARCH 2.0 Faz 1 | DONE | +| 2.0-2 | Graf revizyonu + apply upsert + çatışma çözümleme — SOLARCH 2.0 Faz 2 | DONE | +| 2.0-3 | MCP sunucusu (`solarch-mcp`) — ajanlara bağlam + kural denetimli mutasyon — SOLARCH 2.0 Faz 3 | DONE | + +## Yeni node tipi ekleme + +Üç adım — kuralın kendisi ([spec](docs/specs/) Section 6): + +1. `src/nodes/schemas/.schema.ts` — `BaseNodeSchema.extend({ type: z.literal("Foo"), properties: z.object({...}).strict() }).strict()` +2. `src/nodes/schemas/index.ts` — `NodeSchema` discriminated union'una ekle + `KIND_LABELS` haritasını güncelle +3. `src/nodes/schemas/.schema.spec.ts` — valid + invalid payload örnekleri + +Union'a girmeyen tip TS compile + Zod runtime'da reddedilir. + +## Lisans + +Henüz lisanslanmadı. diff --git a/apps/server/deploy/Caddyfile b/apps/server/deploy/Caddyfile new file mode 100644 index 0000000..5c7ee6a --- /dev/null +++ b/apps/server/deploy/Caddyfile @@ -0,0 +1,45 @@ +# Solarch — tek-origin reverse proxy (Caddy). /etc/caddy/Caddyfile'a kopyala. +# DOMAIN'i gerçek alan adınla değiştir. Caddy otomatik HTTPS (Let's Encrypt) yapar. +# +# Neden tek origin: Clerk __session httpOnly+Secure cookie ancak frontend ile /api AYNI +# origin'de akar. Bu yüzden hem frontend (statik) hem /api buradan servis edilir. + +# www → apex yönlendirme (Clerk authorizedParties tek host bekler, www kopukluğu olmasın). +www.DOMAIN { + redir https://DOMAIN{uri} permanent +} + +DOMAIN { + encode zstd gzip + + # /api/* → backend (yalnız loopback'te dinler). Caddy X-Forwarded-* ekler; + # backend trust proxy=1 ile gerçek IP'yi alır (rate-limit doğru çalışır). + handle /api/* { + reverse_proxy 127.0.0.1:4000 + } + + # Geri kalan → Vite statik build (SPA). try_files derin route'ları index.html'e düşürür. + handle { + root * /var/www/solarch/dist + try_files {path} /index.html + file_server + + # Hash'li asset'ler immutable; index.html no-cache (yeni deploy'da eski HTML cache'lenip + # kırık asset referansı vermesin). + @assets path /assets/* + header @assets Cache-Control "public, max-age=31536000, immutable" + @html path /index.html / + header @html Cache-Control "no-cache" + } + + header { + -Server + Referrer-Policy strict-origin-when-cross-origin + X-Content-Type-Options nosniff + X-Frame-Options DENY + } + + log { + output file /var/log/caddy/solarch.log + } +} diff --git a/apps/server/deploy/apache-app.solarch.dev-le-ssl.conf b/apps/server/deploy/apache-app.solarch.dev-le-ssl.conf new file mode 100644 index 0000000..75cf74b --- /dev/null +++ b/apps/server/deploy/apache-app.solarch.dev-le-ssl.conf @@ -0,0 +1,42 @@ +# Solarch HTTPS — certbot --apache ile üretilir/günceller. Elle kopyalarsan sertifika yollarını doğrula. +# certbot sonrası X-Forwarded-Proto https olmalı (aşağıdaki gibi). + + + + ServerName app.solarch.dev + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/solarch/dist + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + FallbackResource /index.html + + + ProxyPreserveHost On + ProxyPass /api http://127.0.0.1:4001/api + ProxyPassReverse /api http://127.0.0.1:4001/api + + RequestHeader set X-Forwarded-Proto "https" + RequestHeader set X-Forwarded-Port "443" + + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "DENY" + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + Header set Cache-Control "no-cache" + + + ErrorLog ${APACHE_LOG_DIR}/app.solarch.dev-error.log + CustomLog ${APACHE_LOG_DIR}/app.solarch.dev-access.log combined + + SSLCertificateFile /etc/letsencrypt/live/app.solarch.dev/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/app.solarch.dev/privkey.pem + Include /etc/letsencrypt/options-ssl-apache.conf + + diff --git a/apps/server/deploy/apache-app.solarch.dev.conf b/apps/server/deploy/apache-app.solarch.dev.conf new file mode 100644 index 0000000..72d7672 --- /dev/null +++ b/apps/server/deploy/apache-app.solarch.dev.conf @@ -0,0 +1,51 @@ +# Solarch — app.solarch.dev (Apache). AKD (antakyaamerikankultur.com) ayrı default vhost'ta kalır. +# Kurulum: +# sudo cp deploy/apache-app.solarch.dev.conf /etc/apache2/sites-available/app.solarch.dev.conf +# sudo a2ensite app.solarch.dev.conf +# sudo mkdir -p /var/www/solarch/dist +# sudo cp deploy/placeholder-index.html /var/www/solarch/dist/index.html # build öncesi +# sudo apache2ctl configtest && sudo systemctl reload apache2 +# sudo certbot --apache -d app.solarch.dev +# +# Backend loopback :4001 (AKD :4000'ü kullanır). .env: PORT=4001 + + + ServerName app.solarch.dev + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/solarch/dist + + Options -Indexes +FollowSymLinks + AllowOverride None + Require all granted + FallbackResource /index.html + + + # /api/* → Solarch NestJS (global prefix api/v1 → /api/v1/...) + ProxyPreserveHost On + ProxyPass /api http://127.0.0.1:4001/api + ProxyPassReverse /api http://127.0.0.1:4001/api + + RequestHeader set X-Forwarded-Proto "http" + RequestHeader set X-Forwarded-Port "80" + + Header always set Referrer-Policy "strict-origin-when-cross-origin" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Frame-Options "DENY" + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + Header set Cache-Control "no-cache" + + + ErrorLog ${APACHE_LOG_DIR}/app.solarch.dev-error.log + CustomLog ${APACHE_LOG_DIR}/app.solarch.dev-access.log combined + + # certbot --apache HTTPS kurduktan sonra HTTP→HTTPS yönlendirmesi ekler. + # Manuel ekleme (certbot öncesi): + # RewriteEngine on + # RewriteCond %{SERVER_NAME} =app.solarch.dev + # RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] + diff --git a/apps/server/deploy/placeholder-index.html b/apps/server/deploy/placeholder-index.html new file mode 100644 index 0000000..877247c --- /dev/null +++ b/apps/server/deploy/placeholder-index.html @@ -0,0 +1,381 @@ + + + + + + + Solarch + + + + + + + + + +
+
+ +
+ + + + solarch — deploy +
+ +
+

SOLARCH

+ + + +

Almost there

+

The architecture canvas and AI assistant are being deployed. Check back shortly.

+ +
+ solarch@deploy:~$ + building + +
+ + +
+

System Status

+ +
+ API + + checking + +
+ +
+ Neo4j + + checking + +
+ +
+ Frontend + + deploying + +
+ +

+
+ +

// frontend deployment in progress

+ + + +
+ +
+
+ + + + diff --git a/apps/server/deploy/solarch-backend.service b/apps/server/deploy/solarch-backend.service new file mode 100644 index 0000000..0a4bf20 --- /dev/null +++ b/apps/server/deploy/solarch-backend.service @@ -0,0 +1,25 @@ +# Solarch backend (NestJS) — /etc/systemd/system/solarch-backend.service +# Kullanıcı/yolları sunucuna göre düzelt. Etkinleştir: +# sudo systemctl daemon-reload && sudo systemctl enable --now solarch-backend + +[Unit] +Description=Solarch backend (NestJS) +After=network-online.target docker.service +Wants=network-online.target + +[Service] +Type=simple +User=solarch +WorkingDirectory=/opt/solarch/solarch-backend +# 40+ anahtarlı backend .env (Clerk/Neo4j/Polar/DeepSeek) — 600 izinli, repo dışı. +# Apache (AKD :4000 dolu): PORT=4001 +EnvironmentFile=/opt/solarch/solarch-backend/.env +ExecStart=/usr/bin/node dist/main.js +Restart=on-failure +RestartSec=3 +# enableShutdownHooks (Tema 6.2) graceful kapanışa süre tanı (Neo4j driver.close + in-flight). +TimeoutStopSec=20 +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target diff --git a/apps/server/deploy/solarch-neo4j-backup.service b/apps/server/deploy/solarch-neo4j-backup.service new file mode 100644 index 0000000..4062516 --- /dev/null +++ b/apps/server/deploy/solarch-neo4j-backup.service @@ -0,0 +1,9 @@ +# Neo4j gunluk yedek — /etc/systemd/system/solarch-neo4j-backup.service +# Timer ile tetiklenir (solarch-neo4j-backup.timer). Yol/kullanıcıyı düzelt. +[Unit] +Description=Solarch Neo4j gunluk yedek +After=docker.service + +[Service] +Type=oneshot +ExecStart=/opt/solarch/solarch-backend/scripts/neo4j-backup.sh diff --git a/apps/server/deploy/solarch-neo4j-backup.timer b/apps/server/deploy/solarch-neo4j-backup.timer new file mode 100644 index 0000000..910285c --- /dev/null +++ b/apps/server/deploy/solarch-neo4j-backup.timer @@ -0,0 +1,11 @@ +# Neo4j yedek zamanlayıcı — /etc/systemd/system/solarch-neo4j-backup.timer +# Etkinleştir: sudo systemctl enable --now solarch-neo4j-backup.timer +[Unit] +Description=Solarch Neo4j yedek zamanlayici (gunluk 04:00) + +[Timer] +OnCalendar=*-*-* 04:00:00 +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/apps/server/docker-compose.yml b/apps/server/docker-compose.yml new file mode 100644 index 0000000..6962114 --- /dev/null +++ b/apps/server/docker-compose.yml @@ -0,0 +1,30 @@ +services: + neo4j: + image: neo4j:5-community + container_name: solarch-neo4j + # SADECE loopback'e bağla — Neo4j dışarıdan erişilemez (backend aynı hostta bolt://localhost). + # Prod güvenlik: 7474/7687 ASLA 0.0.0.0'a açılmamalı. + ports: + - "127.0.0.1:7474:7474" + - "127.0.0.1:7687:7687" + environment: + # Parola .env'den (backend dizinindeki .env compose tarafından okunur). Dev default + # solarch_dev_password; PROD'da .env'e güçlü NEO4J_PASSWORD koy. NOT: parola yalnız + # volume İLK init'te yazılır — mevcut volume'da değiştirmek için reset gerekir. + NEO4J_AUTH: neo4j/${NEO4J_PASSWORD:-solarch_dev_password} + NEO4J_PLUGINS: '["apoc"]' + # Bellek tuning (2vCPU/8GB tek-kutu: Neo4j + backend + Caddy paylaşır). Gerekirse artır. + NEO4J_server_memory_heap_initial__size: ${NEO4J_HEAP:-512m} + NEO4J_server_memory_heap_max__size: ${NEO4J_HEAP:-512m} + NEO4J_server_memory_pagecache_size: ${NEO4J_PAGECACHE:-512m} + volumes: + - solarch_neo4j_data:/data + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:7474"] + interval: 5s + timeout: 3s + retries: 20 + restart: unless-stopped + +volumes: + solarch_neo4j_data: diff --git a/apps/server/docker-entrypoint.sh b/apps/server/docker-entrypoint.sh new file mode 100644 index 0000000..76a1443 --- /dev/null +++ b/apps/server/docker-entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Solarch server entrypoint: idempotently bootstrap the graph DB, then start the API. +# Neo4j readiness is guaranteed by the compose healthcheck (depends_on: service_healthy); +# every step is idempotent (schema uses IF NOT EXISTS, the seed uses MERGE), so a restart +# is safe. Each step is best-effort — a failure is logged but never blocks the server. +set -e +cd /app/apps/server +TSX="node_modules/.bin/tsx" + +echo "[solarch] initializing graph database (idempotent) ..." +$TSX src/neo4j/migrations/run.ts || echo "[solarch] WARN: schema migration step failed" +$TSX src/neo4j/migrations/data/004-pattern-vector-index.ts || echo "[solarch] WARN: pattern vector index step failed" +$TSX src/patterns/seed/seed.ts || echo "[solarch] WARN: pattern seed step failed" + +echo "[solarch] starting API on ${HOST:-0.0.0.0}:${PORT:-4000} ..." +exec node dist/main.js diff --git a/apps/server/docs/ops/backup-restore.md b/apps/server/docs/ops/backup-restore.md new file mode 100644 index 0000000..8a46552 --- /dev/null +++ b/apps/server/docs/ops/backup-restore.md @@ -0,0 +1,77 @@ +# Neo4j Yedek & Restore (Solarch — self-host, Neo4j 5 Community) + +## Neden offline? +Neo4j **Community** edition'da online `backup` subcommand'ı ve çalışan DB üzerinde +`dump` **yoktur** (`The database is in use` hatası). Tek güvenli yol: container'ı +kısa süre durdur → ephemeral container ile **offline dump** → tekrar başlat. +Kesinti penceresi küçük DB'de birkaç saniyedir; gece düşük trafikte (04:00) çalıştır. + +> Büyük DB / sıfır-kesinti gerekirse Neo4j **Enterprise** online-backup veya read-replica +> değerlendirilmeli. Launch ölçeğinde offline dump yeterli. + +## Dosyalar +- `scripts/neo4j-backup.sh` — günlük yedek (stop → dump system+neo4j → start → health → + retention → opsiyonel offsite). **trap ile her çıkışta container yeniden başlatılır.** +- `scripts/neo4j-restore.sh [DIZIN]` — bir yedeği geri yükler (varsayılan `backups/latest`). +- `scripts/neo4j-backup.env.example` — kopyala → `scripts/neo4j-backup.env` (gitignore'lu). + +Yedekler `backups//` altında `system.dump.gz` + `neo4j.dump.gz`. +`backups/` ve `scripts/neo4j-backup.env` **gitignore'lu** (dump PII içerir). + +## Kurulum (sunucu) + +### systemd timer (önerilir — kaçırılan çalıştırmayı telafi eder) +`/etc/systemd/system/solarch-neo4j-backup.service`: +```ini +[Unit] +Description=Solarch Neo4j gunluk yedek +[Service] +Type=oneshot +ExecStart=/home/USER/solarch-backend/scripts/neo4j-backup.sh +``` +`/etc/systemd/system/solarch-neo4j-backup.timer`: +```ini +[Unit] +Description=Solarch Neo4j yedek zamanlayici +[Timer] +OnCalendar=*-*-* 04:00:00 +Persistent=true +[Install] +WantedBy=timers.target +``` +```bash +sudo systemctl enable --now solarch-neo4j-backup.timer +sudo systemctl list-timers | grep solarch # doğrula +journalctl -u solarch-neo4j-backup.service # log +``` + +### cron (alternatif — root, docker erişimi için) +```cron +0 4 * * * /home/USER/solarch-backend/scripts/neo4j-backup.sh >> /var/log/solarch-neo4j-backup.log 2>&1 +``` + +## Offsite (önerilir) +```bash +rclone config # B2 / GCS / S3 remote tanımla +echo 'RCLONE_REMOTE=b2:solarch-backups' >> scripts/neo4j-backup.env +``` +Offsite hedef erişimi kısıtlı/şifreli olmalı (dump tüm veriyi taşır). + +## Restore +```bash +scripts/neo4j-restore.sh # backups/latest +scripts/neo4j-restore.sh backups/20260602-040000 +# 'evet' onayı → stop → load system+neo4j (--overwrite) → start +pnpm neo4j:migrate # restore SONRASI şema migration'larını çalıştır +``` + +## Doğrulama (ilk kurulumda mutlaka) +1. `scripts/neo4j-backup.sh` çalıştır → `backups//` altında iki `.dump.gz` (>0 byte), container `healthy`. +2. **Trap testi (kritik):** `neo4j-backup.env`'de geçici `IMAGE=yok:lmage` yap → script hata verir AMA `docker ps` container'ı RUNNING gösterir. (Sonra geri al.) +3. **Round-trip:** test node ekle → backup → node'u sil → `neo4j-restore.sh latest` → node geri geldi mi. + +## Felaket kurtarma checklist +- [ ] offsite yedek erişilebilir mi (rclone ls) +- [ ] doğru gün/saat yedeği seç +- [ ] restore + `pnpm neo4j:migrate` +- [ ] smoke: giriş, proje listesi, bir AI üretim, webhook diff --git a/apps/server/docs/ops/deploy.md b/apps/server/docs/ops/deploy.md new file mode 100644 index 0000000..3477378 --- /dev/null +++ b/apps/server/docs/ops/deploy.md @@ -0,0 +1,110 @@ +# Solarch — Deploy Runbook (Hostinger KVM2, tek-kutu self-host) + +Tek sunucuda: **Caddy** (tek origin, HTTPS) → `/` Vite statik dist + `/api/*` → backend (loopback :4000); +**Neo4j** (Docker, yalnız localhost); **backend** (systemd, `node dist/main.js`). + +> Artefaktlar `deploy/` altında: `Caddyfile`, `solarch-backend.service`, `solarch-neo4j-backup.{service,timer}`. +> Yedek/restore: `scripts/neo4j-backup.sh` + `docs/ops/backup-restore.md`. + +## 0) Ön koşullar (sağlayıcı panelleri — env YETMEZ) +Bu zincirden BİR halka koparsa **giriş tamamen çalışmaz** (semptomlar yanıltıcı: 401/CORS/cookie): +- [ ] **DNS**: `DOMAIN` ve `www.DOMAIN` A kaydı → sunucu IP'si. +- [ ] **Clerk (production instance)**: pk_live/sk_live al; Clerk panelinde **custom domain / Frontend API** DNS kayıtlarını (CNAME `clerk.DOMAIN` vb.) kur; **Allowed origins / redirect URLs** = `https://DOMAIN`. (pk_live, custom domain kurulmadan cookie akmaz.) +- [ ] **Polar (production)**: ürünler production org'da; webhook endpoint = `https://DOMAIN/api/v1/billing/webhook`; **production webhook secret**'ı al (sandbox'tan farklı); organization access token oluştur. +- [ ] **HTTPS ŞART**: Clerk pk_live `Secure` cookie ister → HTTP/IP ile giriş çalışmaz. Caddy otomatik Let's Encrypt (port 80 ACME challenge'ı görmeli → ufw'de açık). + +## 1) Sunucu hazırlık +```bash +# Docker + Caddy + node + pnpm kurulu olsun. ufw: +sudo ufw allow 22 && sudo ufw allow 80 && sudo ufw allow 443 +sudo ufw enable # 22'yi AÇMADAN enable etme (kendini kilitleme) +# Neo4j 7474/7687 ufw'de AÇILMAZ — zaten compose 127.0.0.1'e bağlı. +``` + +## 2) Kod + env +```bash +sudo mkdir -p /opt/solarch && cd /opt/solarch +git clone solarch-backend && git clone solarch-frontend +``` +**backend `.env`** (`/opt/solarch/solarch-backend/.env`, `chmod 600`, repo dışı): +``` +NODE_ENV=production +PORT=4000 +NEO4J_URI=bolt://localhost:7687 +NEO4J_PASSWORD= # compose ilk init'te bunu kullanır +CORS_ORIGIN=https://DOMAIN # same-origin'de tetiklenmez ama doğru set et +CLERK_SECRET_KEY=sk_live_... +CLERK_PUBLISHABLE_KEY=pk_live_... +CLERK_AUTHORIZED_PARTIES=https://DOMAIN # CSRF — www apex'e redirect edildiği için tek host +POLAR_SERVER=production +POLAR_ACCESS_TOKEN=polar_oat_...(production) +POLAR_WEBHOOK_SECRET=whsec_...(production) +POLAR_PRODUCT_DRAW/BUILD/CODE=prod_...(production) +DEEPSEEK_API_KEY=... (veya BEDROCK_*) +``` +**frontend `.env`** (build-time; `pnpm build` ÖNCESİ doğru olmalı — bundle'a gömülür): +``` +VITE_API_URL= # BOŞ! same-origin /api/v1 (Clerk cookie için ŞART) +VITE_CLERK_PUBLISHABLE_KEY=pk_live_... +# Billing: Polar hosted-redirect checkout — backend creates the session, no client billing keys needed. +``` +> Secret aktarımı: `.env`'leri **manuel** (scp/panel) koy, repo'ya/CI'a koyma. CI yalnız lint/build/test gate'i — deploy değil. + +## 3) Neo4j (ilk kurulum — parola volume'a yazılır) +```bash +cd /opt/solarch/solarch-backend +docker compose up -d # NEO4J_PASSWORD .env'den okunur, volume init olur +# healthy bekle: +until [ "$(docker inspect -f '{{.State.Health.Status}}' solarch-neo4j)" = healthy ]; do sleep 2; done +pnpm install --frozen-lockfile +pnpm neo4j:migrate # constraint/index (idempotent — IF NOT EXISTS) +# (opsiyonel veri: pnpm migrate:data:* / seed:patterns) +``` +> Parola değişimi mevcut volume'da ÇALIŞMAZ (Neo4j sadece ilk init'te yazar). Yanlış parolayla +> init olduysa: `docker compose down -v` (VERİ GİDER) → doğru NEO4J_PASSWORD ile tekrar up + migrate. + +## 4) Backend (systemd) +```bash +cd /opt/solarch/solarch-backend && pnpm build # dist/main.js +sudo cp deploy/solarch-backend.service /etc/systemd/system/ +# (User/WorkingDirectory/EnvironmentFile yollarını düzelt) +sudo systemctl daemon-reload && sudo systemctl enable --now solarch-backend +curl -s 127.0.0.1:4000/api/v1/health/ready # {status:ready} bekle (backend 127.0.0.1'e bind — localhost DEĞİL) +``` + +## 5) Frontend + Caddy +```bash +cd /opt/solarch/solarch-frontend && pnpm install --frozen-lockfile && pnpm build +sudo mkdir -p /var/www/solarch && sudo cp -r dist /var/www/solarch/ +sudo cp deploy/Caddyfile /etc/caddy/Caddyfile # DOMAIN'i düzelt (backend'deki deploy/) +sudo systemctl reload caddy # otomatik HTTPS +``` + +## 6) Otomatik yedek +```bash +sudo cp deploy/solarch-neo4j-backup.{service,timer} /etc/systemd/system/ +sudo systemctl enable --now solarch-neo4j-backup.timer +# Detay + offsite + restore: docs/ops/backup-restore.md. Yedek container'ı kısa stop/start +# eder → o pencerede /health/ready 503 (backend Restart ile toparlar). Gece 04:00. +``` + +## 7) Smoke test (sıralı — kırılgan auth zinciri) +1. `https://DOMAIN` açılır (HTTPS yeşil, www → apex redirect). +2. Kayıt ol → e-posta doğrula → `/start` (login loop YOK → Clerk domain + cookie + AUTHORIZED_PARTIES doğru). +3. Proje oluştur, node ekle, iki node'u bağla (canvas). +4. AI üretim çalışır (DeepSeek/Bedrock anahtarı). +5. `/billing` → Build satın al (Polar prod test) → webhook → plan aktif. +6. `curl https://DOMAIN/api/v1/health/ready` → 200; `docker stop solarch-neo4j` → 503; tekrar start → 200. +7. `sudo systemctl stop solarch-backend` → loglarda graceful shutdown (Neo4j driver.close). + +## Güncelleme (redeploy) +```bash +cd /opt/solarch/solarch-backend && git pull && pnpm install --frozen-lockfile && pnpm build && pnpm neo4j:migrate && sudo systemctl restart solarch-backend +cd /opt/solarch/solarch-frontend && git pull && pnpm install --frozen-lockfile && pnpm build && sudo cp -r dist/* /var/www/solarch/dist/ +``` +> Tek instance → restart sırasında kısa (~saniye) 502 penceresi kaçınılmaz (zero-downtime yok). + +## Bilinen sınırlar +- Tek-kutu = tek hata noktası + HA yok. Neo4j heap/pagecache compose'da 512m (8GB'ı backend ile paylaşır; yük artarsa NEO4J_HEAP/NEO4J_PAGECACHE env ile artır). +- CI gate Neo4j repository/migration regresyonlarını yakalamaz (testcontainers nightly'de) — deploy öncesi `pnpm test:docker` + `pnpm test:e2e` lokalde koş. +- Frontend lint-debt (~30 react-hooks uyarısı) gate'te blocking değil. diff --git a/apps/server/docs/plans/2026-05-21-node-types-implementation.md b/apps/server/docs/plans/2026-05-21-node-types-implementation.md new file mode 100644 index 0000000..489ce3c --- /dev/null +++ b/apps/server/docs/plans/2026-05-21-node-types-implementation.md @@ -0,0 +1,3334 @@ +# Phase 1 — Node Types Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** `solarch-backend` repo'suna NestJS + Zod + Neo4j stack'i kuran ve Phase 1 kapsamında 5 Veri ailesi node tipini (Table, DTO, Model, Enum, View) tam CRUD endpoint'leri ile sunulan production-grade bir backend implement et. + +**Architecture:** NestJS modüler yapı + Zod discriminated union ile property-level şema validasyonu + Neo4j (raw Cypher) ile persistence. Her node tipi `BaseNodeSchema`'yı extend eder; yeni tip ekleme üç adıma kodlanmıştır (schema dosyası + union'a kayıt + KIND_LABELS). Validation pipeline pipe → controller → service → repository → DB akışındadır; hata envelope'ları plans/API Spec'inden birebir. + +**Tech Stack:** +- Node.js 22 LTS + TypeScript 5.x +- NestJS 11 (Express adapter) +- Zod 3.x +- neo4j-driver 5.x (raw Cypher, OGM yok) +- pnpm 10 +- Vitest 2.x (Jest yerine — modern, ESM-friendly) +- supertest (HTTP e2e) +- Testcontainers 10.x (geçici Neo4j container) +- Docker Compose (lokal Neo4j 5-community + APOC) + +**Spec:** [`docs/specs/2026-05-21-node-types-design.md`](../specs/2026-05-21-node-types-design.md) + +**Çalışma dizini:** Tüm path'ler ve git komutları `~/Masaüstü/Arsiv/solarch-backend/` kökünden çalıştırılır. + +--- + +## File Structure + +``` +solarch-backend/ +├── package.json Task 1 +├── pnpm-lock.yaml Task 1 (otomatik) +├── tsconfig.json Task 1 +├── tsconfig.build.json Task 1 +├── nest-cli.json Task 1 +├── vitest.config.ts Task 2 +├── vitest.e2e.config.ts Task 20 +├── docker-compose.yml Task 3 +├── .env.example Task 3 +├── src/ +│ ├── main.ts Task 1 +│ ├── app.module.ts Task 1 +│ ├── config/ +│ │ └── env.ts Task 4 +│ ├── neo4j/ +│ │ ├── neo4j.module.ts Task 5 +│ │ ├── neo4j.service.ts Task 5 +│ │ ├── neo4j.service.spec.ts Task 5 +│ │ └── migrations/ +│ │ ├── 001_constraints.cypher Task 5 +│ │ └── run.ts Task 5 +│ ├── nodes/ +│ │ ├── nodes.module.ts Task 17 +│ │ ├── nodes.controller.ts Task 17 +│ │ ├── nodes.service.ts Task 16 +│ │ ├── nodes.service.spec.ts Task 16 +│ │ ├── nodes.repository.ts Task 15 +│ │ ├── nodes.repository.spec.ts Task 15 +│ │ ├── schemas/ +│ │ │ ├── base.schema.ts Task 6 +│ │ │ ├── base.schema.spec.ts Task 6 +│ │ │ ├── table.schema.ts Task 7 +│ │ │ ├── table.schema.spec.ts Task 7 +│ │ │ ├── dto.schema.ts Task 8 +│ │ │ ├── dto.schema.spec.ts Task 8 +│ │ │ ├── model.schema.ts Task 9 +│ │ │ ├── model.schema.spec.ts Task 9 +│ │ │ ├── enum.schema.ts Task 10 +│ │ │ ├── enum.schema.spec.ts Task 10 +│ │ │ ├── view.schema.ts Task 11 +│ │ │ ├── view.schema.spec.ts Task 11 +│ │ │ └── index.ts Task 11 +│ │ └── dto/ +│ │ ├── create-node.dto.ts Task 17 +│ │ ├── update-node.dto.ts Task 19 +│ │ └── node-response.dto.ts Task 17 +│ ├── common/ +│ │ ├── envelope.ts Task 12 +│ │ ├── pipes/ +│ │ │ ├── zod-validation.pipe.ts Task 12 +│ │ │ └── zod-validation.pipe.spec.ts Task 12 +│ │ └── filters/ +│ │ ├── schema-error.filter.ts Task 13 +│ │ ├── schema-error.filter.spec.ts Task 13 +│ │ ├── not-found.filter.ts Task 14 +│ │ ├── conflict.filter.ts Task 14 +│ │ └── internal.filter.ts Task 14 +│ └── health/ +│ ├── health.controller.ts Task 21 +│ └── health.controller.spec.ts Task 21 +├── test/ +│ └── nodes.e2e-spec.ts Task 20 +└── README.md Task 21 (update) +``` + +**Decomposition kararı:** Her dosya tek bir sorumluluk taşır. Schema dosyaları kind başına ayrı; pipe/filter'lar tek-amaçlı; repository sadece Cypher, service sadece business logic, controller sadece HTTP. Hiçbir dosya ~150 satırı aşmamalı. + +--- + +# Sprint 1 — Boilerplate ve Altyapı + +## Task 1: NestJS app skeleton + TypeScript config + +**Files:** +- Create: `package.json` +- Create: `tsconfig.json` +- Create: `tsconfig.build.json` +- Create: `nest-cli.json` +- Create: `src/main.ts` +- Create: `src/app.module.ts` + +- [ ] **Step 1: package.json yaz** + +```json +{ + "name": "solarch-backend", + "version": "0.1.0", + "description": "Solarch architecture graph backend — Node CRUD + Rules Engine", + "private": true, + "scripts": { + "build": "nest build", + "dev": "nest start --watch", + "start": "node dist/main.js", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "vitest run --config vitest.e2e.config.ts", + "lint": "eslint src --ext .ts", + "neo4j:up": "docker compose up -d", + "neo4j:down": "docker compose down", + "neo4j:migrate": "tsx src/neo4j/migrations/run.ts" + }, + "dependencies": { + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "neo4j-driver": "^5.27.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@nestjs/cli": "^11.0.0", + "@nestjs/testing": "^11.0.0", + "@types/node": "^22.10.0", + "@types/supertest": "^6.0.2", + "supertest": "^7.0.0", + "testcontainers": "^10.16.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "packageManager": "pnpm@10.0.0" +} +``` + +- [ ] **Step 2: tsconfig.json yaz** + +```json +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2022", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +- [ ] **Step 3: tsconfig.build.json yaz** + +```json +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*.spec.ts"] +} +``` + +- [ ] **Step 4: nest-cli.json yaz** + +```json +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} +``` + +- [ ] **Step 5: src/app.module.ts yaz** + +```ts +import { Module } from "@nestjs/common"; + +@Module({}) +export class AppModule {} +``` + +- [ ] **Step 6: src/main.ts yaz** + +```ts +import "reflect-metadata"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix("api/v1"); + app.enableCors({ + origin: process.env.CORS_ORIGIN ?? "http://localhost:3000", + credentials: false, + }); + const port = Number(process.env.PORT ?? 4000); + await app.listen(port); + console.log(`solarch-backend listening on http://localhost:${port}`); +} + +bootstrap(); +``` + +- [ ] **Step 7: Bağımlılıkları kur** + +Run: `pnpm install` +Expected: `node_modules/` oluşur, `pnpm-lock.yaml` üretilir, hata yok. + +- [ ] **Step 8: Build sanity check** + +Run: `pnpm build` +Expected: `dist/main.js` oluşur, hata yok. + +- [ ] **Step 9: Commit** + +```bash +git add package.json pnpm-lock.yaml tsconfig.json tsconfig.build.json nest-cli.json src/main.ts src/app.module.ts +git commit -m "chore(scaffold): NestJS app skeleton + TypeScript config + +Phase 1 boilerplate: NestJS 11 + TypeScript 5 + pnpm 10. Global prefix +'api/v1' ve env-driven CORS main.ts'te aktif. Henüz endpoint yok." +``` + +--- + +## Task 2: Vitest setup + sanity test + +**Files:** +- Create: `vitest.config.ts` +- Create: `src/sanity.spec.ts` (silinecek) + +- [ ] **Step 1: vitest.config.ts yaz** + +```ts +import { defineConfig } from "vitest/config"; +import path from "node:path"; + +export default defineConfig({ + test: { + include: ["src/**/*.spec.ts"], + environment: "node", + globals: false, + testTimeout: 10000, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); +``` + +- [ ] **Step 2: Failing sanity test yaz** + +```ts +// src/sanity.spec.ts +import { describe, it, expect } from "vitest"; + +describe("sanity", () => { + it("vitest çalışıyor mu?", () => { + expect(1 + 1).toBe(2); + }); +}); +``` + +- [ ] **Step 3: Test çalıştır** + +Run: `pnpm test` +Expected: 1 test passed, 0 failed. + +- [ ] **Step 4: Sanity dosyasını sil** + +```bash +rm src/sanity.spec.ts +``` + +- [ ] **Step 5: Commit** + +```bash +git add vitest.config.ts +git commit -m "chore(test): vitest config — alias + 10s timeout" +``` + +--- + +## Task 3: Docker Compose Neo4j + .env.example + +**Files:** +- Create: `docker-compose.yml` +- Create: `.env.example` + +- [ ] **Step 1: docker-compose.yml yaz** + +```yaml +services: + neo4j: + image: neo4j:5-community + container_name: solarch-neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + NEO4J_AUTH: neo4j/solarch_dev_password + NEO4J_PLUGINS: '["apoc"]' + volumes: + - solarch_neo4j_data:/data + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:7474"] + interval: 5s + timeout: 3s + retries: 20 + restart: unless-stopped + +volumes: + solarch_neo4j_data: +``` + +- [ ] **Step 2: .env.example yaz** + +```bash +NODE_ENV=development +PORT=4000 + +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=solarch_dev_password + +CORS_ORIGIN=http://localhost:3000 +``` + +- [ ] **Step 3: Neo4j'yi ayağa kaldır** + +Run: `pnpm neo4j:up` +Expected: `solarch-neo4j` container "Up (healthy)" durumunda. + +- [ ] **Step 4: Browser'dan kontrol et** + +Run: `curl -s http://localhost:7474/ | head -c 200` +Expected: JSON response içeren bir gövde (Neo4j HTTP browser). + +- [ ] **Step 5: Container'ı durdur** + +Run: `pnpm neo4j:down` +Expected: container "Stopped". + +- [ ] **Step 6: Commit** + +```bash +git add docker-compose.yml .env.example +git commit -m "chore(infra): docker-compose Neo4j 5-community + APOC + +Lokal Neo4j 7474 (browser) ve 7687 (bolt) portlarında. APOC plugin +otomatik yükleniyor. .env.example sample değerlerle hazır." +``` + +--- + +## Task 4: Config module — Zod ile env validation + +**Files:** +- Create: `src/config/env.ts` +- Create: `src/config/env.spec.ts` +- Modify: `src/main.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/config/env.spec.ts +import { describe, it, expect } from "vitest"; +import { parseEnv } from "./env"; + +describe("parseEnv", () => { + it("eksik NEO4J_URI'de fırlatır", () => { + expect(() => parseEnv({ NEO4J_USER: "neo4j", NEO4J_PASSWORD: "x" })).toThrow(); + }); + + it("geçerli env'i parse eder ve default'ları doldurur", () => { + const env = parseEnv({ + NEO4J_URI: "bolt://localhost:7687", + NEO4J_USER: "neo4j", + NEO4J_PASSWORD: "x", + }); + expect(env.PORT).toBe(4000); + expect(env.NODE_ENV).toBe("development"); + expect(env.CORS_ORIGIN).toBe("http://localhost:3000"); + }); + + it("PORT'u coerce eder (string → number)", () => { + const env = parseEnv({ + NEO4J_URI: "bolt://localhost:7687", + NEO4J_USER: "neo4j", + NEO4J_PASSWORD: "x", + PORT: "5000", + }); + expect(env.PORT).toBe(5000); + }); + + it("geçersiz NEO4J_URI'yi reddeder", () => { + expect(() => parseEnv({ + NEO4J_URI: "not-a-url", + NEO4J_USER: "neo4j", + NEO4J_PASSWORD: "x", + })).toThrow(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/config/env.spec.ts` +Expected: "Cannot find module './env'" hatası. + +- [ ] **Step 3: src/config/env.ts yaz** + +```ts +import { z } from "zod"; + +const EnvSchema = z.object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + PORT: z.coerce.number().int().positive().default(4000), + NEO4J_URI: z.string().url(), + NEO4J_USER: z.string().min(1), + NEO4J_PASSWORD: z.string().min(1), + CORS_ORIGIN: z.string().default("http://localhost:3000"), +}); + +export type Env = z.infer; + +export function parseEnv(source: NodeJS.ProcessEnv | Record): Env { + return EnvSchema.parse(source); +} + +export const env = parseEnv(process.env); +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/config/env.spec.ts` +Expected: 4 tests passed. + +- [ ] **Step 5: main.ts'yi env modülünü kullanacak şekilde güncelle** + +`src/main.ts` içeriğini şu hale getir: + +```ts +import "reflect-metadata"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { env } from "./config/env"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix("api/v1"); + app.enableCors({ origin: env.CORS_ORIGIN, credentials: false }); + await app.listen(env.PORT); + console.log(`solarch-backend listening on http://localhost:${env.PORT}`); +} + +bootstrap(); +``` + +- [ ] **Step 6: Build sanity** + +Run: `pnpm build` +Expected: hata yok. + +- [ ] **Step 7: Commit** + +```bash +git add src/config/env.ts src/config/env.spec.ts src/main.ts +git commit -m "feat(config): Zod ile env validation — fail-fast boot + +Eksik veya hatalı env değişkeniyle uygulama başlamaz. PORT/CORS_ORIGIN +default'ları var. main.ts artık env modülünü kullanıyor." +``` + +--- + +## Task 5: Neo4j module — driver singleton + migration runner + +**Files:** +- Create: `src/neo4j/neo4j.service.ts` +- Create: `src/neo4j/neo4j.service.spec.ts` +- Create: `src/neo4j/neo4j.module.ts` +- Create: `src/neo4j/migrations/001_constraints.cypher` +- Create: `src/neo4j/migrations/run.ts` +- Modify: `src/app.module.ts` + +- [ ] **Step 1: Failing test yaz (Testcontainers ile Neo4j)** + +```ts +// src/neo4j/neo4j.service.spec.ts +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { Neo4jContainer, StartedNeo4jContainer } from "@testcontainers/neo4j"; +import { Neo4jService } from "./neo4j.service"; + +describe("Neo4jService", () => { + let container: StartedNeo4jContainer; + let service: Neo4jService; + + beforeAll(async () => { + container = await new Neo4jContainer("neo4j:5-community").withApoc().start(); + service = new Neo4jService({ + uri: container.getBoltUri(), + user: container.getUsername(), + password: container.getPassword(), + }); + await service.onModuleInit(); + }, 120_000); + + afterAll(async () => { + await service.onModuleDestroy(); + await container.stop(); + }); + + it("ping çalışır (1 dönderir)", async () => { + const result = await service.run("RETURN 1 AS n"); + expect(result.records[0].get("n").toNumber()).toBe(1); + }); + + it("transaction içinde write yapar", async () => { + await service.write(async (tx) => { + await tx.run("CREATE (n:Test {id: 't1'})"); + }); + const result = await service.run("MATCH (n:Test {id: 't1'}) RETURN n.id AS id"); + expect(result.records[0].get("id")).toBe("t1"); + }); +}); +``` + +- [ ] **Step 2: @testcontainers/neo4j'yi devDep olarak ekle** + +Run: `pnpm add -D @testcontainers/neo4j` +Expected: package.json güncellenir, lock dosya güncellenir. + +- [ ] **Step 3: Test fail eder** + +Run: `pnpm test src/neo4j/neo4j.service.spec.ts` +Expected: "Cannot find module './neo4j.service'" hatası. + +- [ ] **Step 4: src/neo4j/neo4j.service.ts yaz** + +```ts +import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; +import neo4j, { Driver, Session, Transaction, QueryResult } from "neo4j-driver"; + +export interface Neo4jConfig { + uri: string; + user: string; + password: string; +} + +@Injectable() +export class Neo4jService implements OnModuleInit, OnModuleDestroy { + private driver!: Driver; + + constructor(private readonly config: Neo4jConfig) {} + + async onModuleInit(): Promise { + this.driver = neo4j.driver( + this.config.uri, + neo4j.auth.basic(this.config.user, this.config.password), + { disableLosslessIntegers: true }, + ); + await this.driver.verifyConnectivity(); + } + + async onModuleDestroy(): Promise { + await this.driver?.close(); + } + + async run(cypher: string, params?: Record): Promise { + const session: Session = this.driver.session(); + try { + return await session.run(cypher, params); + } finally { + await session.close(); + } + } + + async write(work: (tx: Transaction) => Promise): Promise { + const session: Session = this.driver.session(); + try { + return await session.executeWrite(work); + } finally { + await session.close(); + } + } + + async read(work: (tx: Transaction) => Promise): Promise { + const session: Session = this.driver.session(); + try { + return await session.executeRead(work); + } finally { + await session.close(); + } + } +} +``` + +- [ ] **Step 5: src/neo4j/neo4j.module.ts yaz** + +```ts +import { Global, Module } from "@nestjs/common"; +import { Neo4jService } from "./neo4j.service"; +import { env } from "../config/env"; + +@Global() +@Module({ + providers: [ + { + provide: Neo4jService, + useFactory: () => new Neo4jService({ + uri: env.NEO4J_URI, + user: env.NEO4J_USER, + password: env.NEO4J_PASSWORD, + }), + }, + ], + exports: [Neo4jService], +}) +export class Neo4jModule {} +``` + +- [ ] **Step 6: src/app.module.ts güncelle** + +```ts +import { Module } from "@nestjs/common"; +import { Neo4jModule } from "./neo4j/neo4j.module"; + +@Module({ + imports: [Neo4jModule], +}) +export class AppModule {} +``` + +- [ ] **Step 7: Migration cypher dosyasını yaz** + +```cypher +-- src/neo4j/migrations/001_constraints.cypher +CREATE CONSTRAINT node_id_unique IF NOT EXISTS + FOR (n:Node) REQUIRE n.id IS UNIQUE; + +CREATE INDEX node_project_idx IF NOT EXISTS + FOR (n:Node) ON (n.projectId); +``` + +- [ ] **Step 8: Migration runner script yaz** + +```ts +// src/neo4j/migrations/run.ts +import { readFileSync, readdirSync } from "node:fs"; +import { join } from "node:path"; +import { Neo4jService } from "../neo4j.service"; +import { env } from "../../config/env"; + +async function main() { + const service = new Neo4jService({ + uri: env.NEO4J_URI, + user: env.NEO4J_USER, + password: env.NEO4J_PASSWORD, + }); + await service.onModuleInit(); + + const dir = join(__dirname); + const files = readdirSync(dir).filter((f) => f.endsWith(".cypher")).sort(); + + for (const file of files) { + const cypher = readFileSync(join(dir, file), "utf-8"); + const statements = cypher.split(/;\s*$/m).map((s) => s.trim()).filter((s) => s && !s.startsWith("--")); + for (const stmt of statements) { + console.log(`[${file}] ${stmt.slice(0, 80)}...`); + await service.run(stmt); + } + } + + await service.onModuleDestroy(); + console.log("✓ Migrations complete."); +} + +main().catch((err) => { + console.error("✗ Migration failed:", err); + process.exit(1); +}); +``` + +- [ ] **Step 9: Test geçer** + +Run: `pnpm test src/neo4j/neo4j.service.spec.ts` +Expected: 2 tests passed (Testcontainers ilk run'da Docker image indirir, ~2 dakika). + +- [ ] **Step 10: Build sanity** + +Run: `pnpm build` +Expected: hata yok. + +- [ ] **Step 11: Migration manual run** + +Run: `pnpm neo4j:up && sleep 15 && cp .env.example .env && pnpm neo4j:migrate` +Expected: `✓ Migrations complete.` ve 2 statement uygulandı. + +- [ ] **Step 12: Container'ı durdur** + +Run: `pnpm neo4j:down` + +- [ ] **Step 13: Commit** + +```bash +git add src/neo4j/ src/app.module.ts package.json pnpm-lock.yaml +git commit -m "feat(neo4j): driver singleton + migration runner + +Neo4jModule global olarak driver'ı sağlar (verifyConnectivity boot'ta). +run/read/write transaction wrapper'ları; session lifecycle güvenli. +Migration 001 node_id unique constraint + project index kurar. +Testcontainers ile e2e doğrulandı." +``` + +--- + +# Sprint 2 — Node Şemaları + +## Task 6: BaseNodeSchema + Position + +**Files:** +- Create: `src/nodes/schemas/base.schema.ts` +- Create: `src/nodes/schemas/base.schema.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/base.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { BaseNodeSchema, PositionSchema } from "./base.schema"; + +describe("PositionSchema", () => { + it("geçerli position'ı parse eder", () => { + const result = PositionSchema.parse({ x: 150, y: 300 }); + expect(result).toEqual({ x: 150, y: 300 }); + }); + + it("x veya y eksikse fırlatır", () => { + expect(() => PositionSchema.parse({ x: 150 })).toThrow(); + }); + + it("x veya y number değilse fırlatır", () => { + expect(() => PositionSchema.parse({ x: "150", y: 300 })).toThrow(); + }); +}); + +describe("BaseNodeSchema", () => { + const valid = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", + }; + + it("geçerli base node'u parse eder", () => { + expect(() => BaseNodeSchema.parse(valid)).not.toThrow(); + }); + + it("id UUID değilse fırlatır", () => { + expect(() => BaseNodeSchema.parse({ ...valid, id: "abc" })).toThrow(); + }); + + it("createdAt ISO datetime değilse fırlatır", () => { + expect(() => BaseNodeSchema.parse({ ...valid, createdAt: "yesterday" })).toThrow(); + }); + + it("projectId UUID değilse fırlatır", () => { + expect(() => BaseNodeSchema.parse({ ...valid, projectId: "p1" })).toThrow(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/schemas/base.schema.spec.ts` +Expected: "Cannot find module './base.schema'" hatası. + +- [ ] **Step 3: base.schema.ts yaz** + +```ts +import { z } from "zod"; + +export const PositionSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const BaseNodeSchema = z.object({ + id: z.string().uuid(), + projectId: z.string().uuid(), + position: PositionSchema, + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +export type BaseNode = z.infer; +export type Position = z.infer; +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/schemas/base.schema.spec.ts` +Expected: 7 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/base.schema.ts src/nodes/schemas/base.schema.spec.ts +git commit -m "feat(schemas): BaseNodeSchema — yeni tip için zorunlu base alanlar + +id (UUID), projectId (UUID), position (x,y), createdAt/updatedAt (ISO). +Her node tipi bu base'i extend edecek." +``` + +--- + +## Task 7: Table node schema + +**Files:** +- Create: `src/nodes/schemas/table.schema.ts` +- Create: `src/nodes/schemas/table.schema.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/table.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { TableNodeSchema } from "./table.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +const validProperties = { + TableName: "users", + Description: "Kayıtlı kullanıcılar", + Columns: [ + { + Name: "id", + DataType: "UUID", + IsPrimaryKey: true, + IsForeignKey: false, + IsNotNull: true, + IsUnique: true, + AutoIncrement: false, + }, + ], + Indexes: [], +}; + +describe("TableNodeSchema", () => { + it("geçerli Table node'u parse eder", () => { + const node = TableNodeSchema.parse({ ...validBase, type: "Table", properties: validProperties }); + expect(node.type).toBe("Table"); + expect(node.properties.TableName).toBe("users"); + }); + + it("Description eksikse fırlatır", () => { + const { Description, ...rest } = validProperties; + expect(() => TableNodeSchema.parse({ ...validBase, type: "Table", properties: rest })).toThrow(); + }); + + it("Columns boşsa fırlatır", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, Columns: [] }, + })).toThrow(); + }); + + it("Bilinmeyen DataType reddeder", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, Columns: [{ ...validProperties.Columns[0], DataType: "FOOBAR" }] }, + })).toThrow(); + }); + + it("Properties içinde bilinmeyen alanı reddeder (strict)", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, ExtraField: "x" }, + })).toThrow(); + }); + + it("type literal değilse reddeder", () => { + expect(() => TableNodeSchema.parse({ ...validBase, type: "Foo", properties: validProperties })).toThrow(); + }); + + it("Indexes default boş array olur (verilmezse)", () => { + const { Indexes, ...partialProps } = validProperties; + const node = TableNodeSchema.parse({ ...validBase, type: "Table", properties: partialProps }); + expect(node.properties.Indexes).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/schemas/table.schema.spec.ts` +Expected: "Cannot find module './table.schema'" hatası. + +- [ ] **Step 3: table.schema.ts yaz** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const ColumnSchema = z.object({ + Name: z.string().min(1), + DataType: z.enum(["INT", "VARCHAR", "TEXT", "BOOLEAN", "DATETIME", "UUID", "FLOAT", "JSON"]), + Length: z.number().int().positive().optional(), + IsPrimaryKey: z.boolean(), + IsForeignKey: z.boolean(), + References: z.string().optional(), + IsNotNull: z.boolean(), + IsUnique: z.boolean(), + AutoIncrement: z.boolean(), + DefaultValue: z.string().optional(), +}).strict(); + +const IndexSchema = z.object({ + IndexName: z.string().min(1), + Columns: z.array(z.string()).min(1), + Type: z.enum(["B-Tree", "Hash"]), +}).strict(); + +export const TableNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Table"), + properties: z.object({ + TableName: z.string().min(1), + Description: z.string().min(1), + Columns: z.array(ColumnSchema).min(1), + Indexes: z.array(IndexSchema).default([]), + }).strict(), +}).strict(); + +export type TableNode = z.infer; +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/schemas/table.schema.spec.ts` +Expected: 7 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/table.schema.ts src/nodes/schemas/table.schema.spec.ts +git commit -m "feat(schemas): Table node — Columns/Indexes ile plans birebir" +``` + +--- + +## Task 8: DTO node schema + +**Files:** +- Create: `src/nodes/schemas/dto.schema.ts` +- Create: `src/nodes/schemas/dto.schema.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/dto.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { DTONodeSchema } from "./dto.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +const validProperties = { + Name: "CreateUserRequestDTO", + Description: "Yeni kullanıcı kayıt isteği", + Fields: [ + { Name: "email", DataType: "string", IsRequired: true, ValidationRule: "Email", IsArray: false }, + { Name: "age", DataType: "number", IsRequired: false, IsArray: false }, + ], +}; + +describe("DTONodeSchema", () => { + it("geçerli DTO'yu parse eder", () => { + const node = DTONodeSchema.parse({ ...validBase, type: "DTO", properties: validProperties }); + expect(node.properties.Fields).toHaveLength(2); + }); + + it("Description eksikse fırlatır", () => { + const { Description, ...rest } = validProperties; + expect(() => DTONodeSchema.parse({ ...validBase, type: "DTO", properties: rest })).toThrow(); + }); + + it("Fields boşsa fırlatır", () => { + expect(() => DTONodeSchema.parse({ + ...validBase, type: "DTO", + properties: { ...validProperties, Fields: [] }, + })).toThrow(); + }); + + it("Field içinde IsRequired boolean değilse fırlatır", () => { + expect(() => DTONodeSchema.parse({ + ...validBase, type: "DTO", + properties: { ...validProperties, Fields: [{ Name: "x", DataType: "string", IsRequired: "yes", IsArray: false }] }, + })).toThrow(); + }); + + it("ValidationRule opsiyonel", () => { + const node = DTONodeSchema.parse({ ...validBase, type: "DTO", properties: validProperties }); + expect(node.properties.Fields[1].ValidationRule).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/schemas/dto.schema.spec.ts` +Expected: "Cannot find module './dto.schema'" hatası. + +- [ ] **Step 3: dto.schema.ts yaz** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const FieldSchema = z.object({ + Name: z.string().min(1), + DataType: z.string().min(1), + IsRequired: z.boolean(), + ValidationRule: z.string().optional(), + IsArray: z.boolean(), +}).strict(); + +export const DTONodeSchema = BaseNodeSchema.extend({ + type: z.literal("DTO"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + Fields: z.array(FieldSchema).min(1), + }).strict(), +}).strict(); + +export type DTONode = z.infer; +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/schemas/dto.schema.spec.ts` +Expected: 5 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/dto.schema.ts src/nodes/schemas/dto.schema.spec.ts +git commit -m "feat(schemas): DTO node — Fields ile validation rule destekli" +``` + +--- + +## Task 9: Model node schema + +**Files:** +- Create: `src/nodes/schemas/model.schema.ts` +- Create: `src/nodes/schemas/model.schema.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/model.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { ModelNodeSchema } from "./model.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +const validProperties = { + ClassName: "User", + Description: "Kullanıcı entity sınıfı", + Properties: [ + { Name: "id", Type: "UUID" }, + { Name: "email", Type: "string" }, + ], + Methods: [ + { MethodName: "fullName", ReturnType: "string" }, + ], +}; + +describe("ModelNodeSchema", () => { + it("geçerli Model'i parse eder", () => { + const node = ModelNodeSchema.parse({ ...validBase, type: "Model", properties: validProperties }); + expect(node.properties.ClassName).toBe("User"); + }); + + it("Description zorunlu", () => { + const { Description, ...rest } = validProperties; + expect(() => ModelNodeSchema.parse({ ...validBase, type: "Model", properties: rest })).toThrow(); + }); + + it("Properties boşsa fırlatır", () => { + expect(() => ModelNodeSchema.parse({ + ...validBase, type: "Model", + properties: { ...validProperties, Properties: [] }, + })).toThrow(); + }); + + it("Methods default boş array", () => { + const { Methods, ...partial } = validProperties; + const node = ModelNodeSchema.parse({ ...validBase, type: "Model", properties: partial }); + expect(node.properties.Methods).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/schemas/model.schema.spec.ts` +Expected: "Cannot find module './model.schema'" hatası. + +- [ ] **Step 3: model.schema.ts yaz** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const PropertySchema = z.object({ + Name: z.string().min(1), + Type: z.string().min(1), +}).strict(); + +const MethodSchema = z.object({ + MethodName: z.string().min(1), + ReturnType: z.string().min(1), +}).strict(); + +export const ModelNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Model"), + properties: z.object({ + ClassName: z.string().min(1), + Description: z.string().min(1), + Properties: z.array(PropertySchema).min(1), + Methods: z.array(MethodSchema).default([]), + }).strict(), +}).strict(); + +export type ModelNode = z.infer; +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/schemas/model.schema.spec.ts` +Expected: 4 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/model.schema.ts src/nodes/schemas/model.schema.spec.ts +git commit -m "feat(schemas): Model node — Properties/Methods plans birebir" +``` + +--- + +## Task 10: Enum node schema + +**Files:** +- Create: `src/nodes/schemas/enum.schema.ts` +- Create: `src/nodes/schemas/enum.schema.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/enum.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { EnumNodeSchema } from "./enum.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +const validProperties = { + Name: "OrderStatus", + Description: "Sipariş durumu", + Values: ["PENDING", "SHIPPED", "DELIVERED"], +}; + +describe("EnumNodeSchema", () => { + it("geçerli Enum'u parse eder", () => { + const node = EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: validProperties }); + expect(node.properties.Values).toHaveLength(3); + }); + + it("Description zorunlu", () => { + const { Description, ...rest } = validProperties; + expect(() => EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: rest })).toThrow(); + }); + + it("Values boşsa fırlatır", () => { + expect(() => EnumNodeSchema.parse({ + ...validBase, type: "Enum", + properties: { ...validProperties, Values: [] }, + })).toThrow(); + }); + + it("Values içinde boş string reddedilir", () => { + expect(() => EnumNodeSchema.parse({ + ...validBase, type: "Enum", + properties: { ...validProperties, Values: ["A", ""] }, + })).toThrow(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/schemas/enum.schema.spec.ts` +Expected: "Cannot find module './enum.schema'" hatası. + +- [ ] **Step 3: enum.schema.ts yaz** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +export const EnumNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Enum"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + Values: z.array(z.string().min(1)).min(1), + }).strict(), +}).strict(); + +export type EnumNode = z.infer; +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/schemas/enum.schema.spec.ts` +Expected: 4 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/enum.schema.ts src/nodes/schemas/enum.schema.spec.ts +git commit -m "feat(schemas): Enum node — string Values array (plans key-value Phase 2)" +``` + +--- + +## Task 11: View node schema (placeholder) + NodeSchema union + +**Files:** +- Create: `src/nodes/schemas/view.schema.ts` +- Create: `src/nodes/schemas/view.schema.spec.ts` +- Create: `src/nodes/schemas/index.ts` + +- [ ] **Step 1: View test yaz** + +```ts +// src/nodes/schemas/view.schema.spec.ts +import { describe, it, expect } from "vitest"; +import { ViewNodeSchema } from "./view.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +const validProperties = { + ViewName: "active_users_view", + Description: "Aktif kullanıcıları döner", + Definition: "SELECT id, email FROM users WHERE active = true", + SourceTables: ["users"], + Materialized: false, +}; + +describe("ViewNodeSchema", () => { + it("geçerli View'i parse eder", () => { + const node = ViewNodeSchema.parse({ ...validBase, type: "View", properties: validProperties }); + expect(node.properties.SourceTables).toEqual(["users"]); + }); + + it("Definition boşsa fırlatır", () => { + expect(() => ViewNodeSchema.parse({ + ...validBase, type: "View", + properties: { ...validProperties, Definition: "" }, + })).toThrow(); + }); + + it("SourceTables boşsa fırlatır", () => { + expect(() => ViewNodeSchema.parse({ + ...validBase, type: "View", + properties: { ...validProperties, SourceTables: [] }, + })).toThrow(); + }); + + it("Materialized boolean değilse fırlatır", () => { + expect(() => ViewNodeSchema.parse({ + ...validBase, type: "View", + properties: { ...validProperties, Materialized: "no" }, + })).toThrow(); + }); +}); +``` + +- [ ] **Step 2: View test fail eder** + +Run: `pnpm test src/nodes/schemas/view.schema.spec.ts` +Expected: "Cannot find module './view.schema'" hatası. + +- [ ] **Step 3: view.schema.ts yaz** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +// PLACEHOLDER — plans/Node Schemas'ta detay yok; plans güncellenince üzerine yazılır. +export const ViewNodeSchema = BaseNodeSchema.extend({ + type: z.literal("View"), + properties: z.object({ + ViewName: z.string().min(1), + Description: z.string().min(1), + Definition: z.string().min(1), + SourceTables: z.array(z.string()).min(1), + Materialized: z.boolean(), + }).strict(), +}).strict(); + +export type ViewNode = z.infer; +``` + +- [ ] **Step 4: View test geçer** + +Run: `pnpm test src/nodes/schemas/view.schema.spec.ts` +Expected: 4 tests passed. + +- [ ] **Step 5: NodeSchema union için test yaz** + +```ts +// src/nodes/schemas/index.spec.ts +import { describe, it, expect } from "vitest"; +import { NodeSchema, KIND_LABELS, type Node, type NodeKind } from "./index"; + +const baseFields = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", +}; + +describe("NodeSchema (union)", () => { + it("Table tipini parse eder", () => { + const node = NodeSchema.parse({ + ...baseFields, type: "Table", + properties: { + TableName: "u", Description: "d", + Columns: [{ Name: "id", DataType: "UUID", IsPrimaryKey: true, IsForeignKey: false, IsNotNull: true, IsUnique: true, AutoIncrement: false }], + Indexes: [], + }, + }); + expect(node.type).toBe("Table"); + }); + + it("Bilinmeyen type'ı reddeder", () => { + expect(() => NodeSchema.parse({ ...baseFields, type: "Foo", properties: {} })).toThrow(); + }); + + it("KIND_LABELS 5 tipi içerir", () => { + const labels: NodeKind[] = ["Table", "DTO", "Model", "Enum", "View"]; + for (const k of labels) { + expect(KIND_LABELS[k]).toBe(k); + } + }); +}); +``` + +- [ ] **Step 6: index test fail eder** + +Run: `pnpm test src/nodes/schemas/index.spec.ts` +Expected: "Cannot find module './index'" hatası. + +- [ ] **Step 7: index.ts yaz** + +```ts +import { z } from "zod"; +import { TableNodeSchema } from "./table.schema"; +import { DTONodeSchema } from "./dto.schema"; +import { ModelNodeSchema } from "./model.schema"; +import { EnumNodeSchema } from "./enum.schema"; +import { ViewNodeSchema } from "./view.schema"; + +export { BaseNodeSchema, PositionSchema, type BaseNode, type Position } from "./base.schema"; +export { TableNodeSchema, type TableNode } from "./table.schema"; +export { DTONodeSchema, type DTONode } from "./dto.schema"; +export { ModelNodeSchema, type ModelNode } from "./model.schema"; +export { EnumNodeSchema, type EnumNode } from "./enum.schema"; +export { ViewNodeSchema, type ViewNode } from "./view.schema"; + +export const NodeSchema = z.discriminatedUnion("type", [ + TableNodeSchema, + DTONodeSchema, + ModelNodeSchema, + EnumNodeSchema, + ViewNodeSchema, +]); + +export type Node = z.infer; +export type NodeKind = Node["type"]; + +export const KIND_LABELS: Record = { + Table: "Table", + DTO: "DTO", + Model: "Model", + Enum: "Enum", + View: "View", +}; +``` + +- [ ] **Step 8: index test geçer** + +Run: `pnpm test src/nodes/schemas/index.spec.ts` +Expected: 3 tests passed. + +- [ ] **Step 9: Tüm şema testlerini topluca çalıştır** + +Run: `pnpm test src/nodes/schemas/` +Expected: tüm testler geçer (toplam ~27). + +- [ ] **Step 10: Commit** + +```bash +git add src/nodes/schemas/view.schema.ts src/nodes/schemas/view.schema.spec.ts src/nodes/schemas/index.ts src/nodes/schemas/index.spec.ts +git commit -m "feat(schemas): View placeholder + NodeSchema discriminated union + +View plans'ta detay yok; ViewName/Description/Definition/SourceTables/ +Materialized placeholder şeması — plans güncellenince üzerine yazılır. +NodeSchema 5 kind'ı birleştirir, KIND_LABELS Neo4j label whitelist'i." +``` + +--- + +# Sprint 3 — Common Katmanlar + +## Task 12: Envelope helper + ZodValidationPipe + +**Files:** +- Create: `src/common/envelope.ts` +- Create: `src/common/pipes/zod-validation.pipe.ts` +- Create: `src/common/pipes/zod-validation.pipe.spec.ts` + +- [ ] **Step 1: Envelope helper yaz (test'siz — saf data type)** + +```ts +// src/common/envelope.ts +export interface SuccessEnvelope { + success: true; + data: T; +} + +export interface ErrorDetail { + field: string; + issue: string; +} + +export interface ErrorEnvelope { + success: false; + error: { + code: string; + message: string; + details?: ErrorDetail[]; + }; +} + +export function ok(data: T): SuccessEnvelope { + return { success: true, data }; +} + +export function err(code: string, message: string, details?: ErrorDetail[]): ErrorEnvelope { + return { success: false, error: details ? { code, message, details } : { code, message } }; +} +``` + +- [ ] **Step 2: ZodValidationPipe için failing test yaz** + +```ts +// src/common/pipes/zod-validation.pipe.spec.ts +import { describe, it, expect } from "vitest"; +import { z, ZodError } from "zod"; +import { ZodValidationPipe } from "./zod-validation.pipe"; + +const Schema = z.object({ name: z.string(), age: z.number() }).strict(); + +describe("ZodValidationPipe", () => { + it("geçerli body'i transform eder", () => { + const pipe = new ZodValidationPipe(Schema); + expect(pipe.transform({ name: "x", age: 1 })).toEqual({ name: "x", age: 1 }); + }); + + it("invalid body'de ZodError fırlatır", () => { + const pipe = new ZodValidationPipe(Schema); + expect(() => pipe.transform({ name: "x" })).toThrow(ZodError); + }); + + it("bilinmeyen alanda ZodError fırlatır (strict)", () => { + const pipe = new ZodValidationPipe(Schema); + expect(() => pipe.transform({ name: "x", age: 1, extra: "y" })).toThrow(ZodError); + }); +}); +``` + +- [ ] **Step 3: Test fail eder** + +Run: `pnpm test src/common/pipes/zod-validation.pipe.spec.ts` +Expected: "Cannot find module './zod-validation.pipe'" hatası. + +- [ ] **Step 4: zod-validation.pipe.ts yaz** + +```ts +import { Injectable, PipeTransform } from "@nestjs/common"; +import type { ZodSchema } from "zod"; + +@Injectable() +export class ZodValidationPipe implements PipeTransform { + constructor(private readonly schema: ZodSchema) {} + + transform(value: unknown): T { + return this.schema.parse(value); + } +} +``` + +- [ ] **Step 5: Test geçer** + +Run: `pnpm test src/common/pipes/zod-validation.pipe.spec.ts` +Expected: 3 tests passed. + +- [ ] **Step 6: Commit** + +```bash +git add src/common/envelope.ts src/common/pipes/zod-validation.pipe.ts src/common/pipes/zod-validation.pipe.spec.ts +git commit -m "feat(common): envelope helper + ZodValidationPipe + +ok()/err() response shape factory. ZodValidationPipe ZodError fırlatır; +SchemaErrorFilter sonraki task'ta envelope'a çevirecek." +``` + +--- + +## Task 13: SchemaErrorFilter — ERR_SCHEMA_INVALID envelope + +**Files:** +- Create: `src/common/filters/schema-error.filter.ts` +- Create: `src/common/filters/schema-error.filter.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/common/filters/schema-error.filter.spec.ts +import { describe, it, expect, vi } from "vitest"; +import { z, ZodError } from "zod"; +import { SchemaErrorFilter } from "./schema-error.filter"; + +function makeHostMock() { + const json = vi.fn(); + const status = vi.fn(() => ({ json })); + return { + host: { + switchToHttp: () => ({ + getResponse: () => ({ status }), + }), + } as any, + status, + json, + }; +} + +describe("SchemaErrorFilter", () => { + it("ZodError'ı 400 + ERR_SCHEMA_INVALID envelope'una çevirir", () => { + const filter = new SchemaErrorFilter(); + const { host, status, json } = makeHostMock(); + + let zerr: ZodError; + try { + z.object({ name: z.string() }).parse({}); + } catch (e) { + zerr = e as ZodError; + } + + filter.catch(zerr!, host); + + expect(status).toHaveBeenCalledWith(400); + expect(json).toHaveBeenCalledWith({ + success: false, + error: { + code: "ERR_SCHEMA_INVALID", + message: "Gönderilen özellikler şema ile uyuşmuyor.", + details: expect.any(Array), + }, + }); + + const arg = json.mock.calls[0][0]; + expect(arg.error.details).toHaveLength(1); + expect(arg.error.details[0].field).toBe("name"); + }); + + it("nested path'leri dotted string'e çevirir", () => { + const filter = new SchemaErrorFilter(); + const { host, json } = makeHostMock(); + + let zerr: ZodError; + try { + z.object({ properties: z.object({ Columns: z.array(z.object({ Name: z.string() })) }) }) + .parse({ properties: { Columns: [{}] } }); + } catch (e) { + zerr = e as ZodError; + } + + filter.catch(zerr!, host); + const arg = json.mock.calls[0][0]; + expect(arg.error.details[0].field).toBe("properties.Columns.0.Name"); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/common/filters/schema-error.filter.spec.ts` +Expected: "Cannot find module './schema-error.filter'" hatası. + +- [ ] **Step 3: schema-error.filter.ts yaz** + +```ts +import { ArgumentsHost, Catch, ExceptionFilter } from "@nestjs/common"; +import { ZodError } from "zod"; +import { err } from "../envelope"; + +@Catch(ZodError) +export class SchemaErrorFilter implements ExceptionFilter { + catch(exception: ZodError, host: ArgumentsHost): void { + const response = host.switchToHttp().getResponse(); + const details = exception.issues.map((issue) => ({ + field: issue.path.join("."), + issue: issue.message, + })); + response.status(400).json( + err( + "ERR_SCHEMA_INVALID", + "Gönderilen özellikler şema ile uyuşmuyor.", + details, + ), + ); + } +} +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/common/filters/schema-error.filter.spec.ts` +Expected: 2 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/common/filters/schema-error.filter.ts src/common/filters/schema-error.filter.spec.ts +git commit -m "feat(common): SchemaErrorFilter — ZodError → ERR_SCHEMA_INVALID + +Plans/API spec birebir envelope. issue.path dotted (properties.Columns.0.Name)." +``` + +--- + +## Task 14: NotFound + Conflict + Internal filters + global registration + +**Files:** +- Create: `src/common/filters/not-found.filter.ts` +- Create: `src/common/filters/conflict.filter.ts` +- Create: `src/common/filters/internal.filter.ts` +- Modify: `src/main.ts` + +- [ ] **Step 1: not-found.filter.ts yaz** + +```ts +import { ArgumentsHost, Catch, ExceptionFilter, NotFoundException } from "@nestjs/common"; +import { err } from "../envelope"; + +@Catch(NotFoundException) +export class NotFoundFilter implements ExceptionFilter { + catch(exception: NotFoundException, host: ArgumentsHost): void { + const response = host.switchToHttp().getResponse(); + const res = exception.getResponse() as { code?: string; message?: string } | string; + const code = typeof res === "object" && res.code ? res.code : "ERR_NODE_NOT_FOUND"; + const message = typeof res === "object" && res.message ? res.message : "Kayıt bulunamadı."; + response.status(404).json(err(code, message)); + } +} +``` + +- [ ] **Step 2: conflict.filter.ts yaz** + +```ts +import { ArgumentsHost, Catch, ConflictException, ExceptionFilter } from "@nestjs/common"; +import { err } from "../envelope"; + +@Catch(ConflictException) +export class ConflictFilter implements ExceptionFilter { + catch(exception: ConflictException, host: ArgumentsHost): void { + const response = host.switchToHttp().getResponse(); + const res = exception.getResponse() as { code?: string; message?: string } | string; + const code = typeof res === "object" && res.code ? res.code : "ERR_CONFLICT"; + const message = typeof res === "object" && res.message ? res.message : "Çatışma."; + response.status(409).json(err(code, message)); + } +} +``` + +- [ ] **Step 3: internal.filter.ts yaz** + +```ts +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common"; +import { err } from "../envelope"; + +@Catch() +export class InternalFilter implements ExceptionFilter { + private readonly logger = new Logger(InternalFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const response = host.switchToHttp().getResponse(); + + if (exception instanceof HttpException) { + const status = exception.getStatus(); + const res = exception.getResponse() as { code?: string; message?: string } | string; + const code = typeof res === "object" && res.code ? res.code : `ERR_HTTP_${status}`; + const message = typeof res === "object" && res.message + ? res.message + : (typeof res === "string" ? res : exception.message); + response.status(status).json(err(code, message)); + return; + } + + this.logger.error("Beklenmeyen hata", exception instanceof Error ? exception.stack : exception); + response.status(500).json(err("ERR_INTERNAL", "Beklenmeyen bir hata oluştu.")); + } +} +``` + +- [ ] **Step 4: main.ts'de global filter sırasını kur** + +`src/main.ts` içeriğini şu hale getir: + +```ts +import "reflect-metadata"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { env } from "./config/env"; +import { SchemaErrorFilter } from "./common/filters/schema-error.filter"; +import { NotFoundFilter } from "./common/filters/not-found.filter"; +import { ConflictFilter } from "./common/filters/conflict.filter"; +import { InternalFilter } from "./common/filters/internal.filter"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + app.setGlobalPrefix("api/v1"); + app.enableCors({ origin: env.CORS_ORIGIN, credentials: false }); + // En spesifik filter en sonda — NestJS ters sırada uygular. + app.useGlobalFilters( + new InternalFilter(), + new ConflictFilter(), + new NotFoundFilter(), + new SchemaErrorFilter(), + ); + await app.listen(env.PORT); + console.log(`solarch-backend listening on http://localhost:${env.PORT}`); +} + +bootstrap(); +``` + +- [ ] **Step 5: Build sanity** + +Run: `pnpm build` +Expected: hata yok. + +- [ ] **Step 6: Tüm common testler geçiyor** + +Run: `pnpm test src/common/` +Expected: tüm testler geçer. + +- [ ] **Step 7: Commit** + +```bash +git add src/common/filters/not-found.filter.ts src/common/filters/conflict.filter.ts src/common/filters/internal.filter.ts src/main.ts +git commit -m "feat(common): NotFound + Conflict + Internal filter — envelope tutarlı + +NotFoundFilter ERR_NODE_NOT_FOUND default code; HttpException response +{code, message} ise ondan alır. InternalFilter catch-all 500 ERR_INTERNAL, +HttpException'ları kendi status/code'una geri yollar. Global sıra: +Schema → NotFound → Conflict → Internal (en spesifik en önce uygulanır)." +``` + +--- + +# Sprint 4 — Nodes Domain + +## Task 15: NodesRepository — Cypher queries + +**Files:** +- Create: `src/nodes/nodes.repository.ts` +- Create: `src/nodes/nodes.repository.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/nodes.repository.spec.ts +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; +import { Neo4jContainer, StartedNeo4jContainer } from "@testcontainers/neo4j"; +import { Neo4jService } from "../neo4j/neo4j.service"; +import { NodesRepository, type StoredNode } from "./nodes.repository"; + +const projectId = "550e8400-e29b-41d4-a716-446655440001"; +const nodeFixture = (overrides: Partial = {}): StoredNode => ({ + id: "550e8400-e29b-41d4-a716-446655440000", + type: "Table", + projectId, + positionX: 100, + positionY: 200, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", + properties: { TableName: "users", Description: "u", Columns: [], Indexes: [] }, + ...overrides, +}); + +describe("NodesRepository", () => { + let container: StartedNeo4jContainer; + let neo4j: Neo4jService; + let repo: NodesRepository; + + beforeAll(async () => { + container = await new Neo4jContainer("neo4j:5-community").withApoc().start(); + neo4j = new Neo4jService({ + uri: container.getBoltUri(), + user: container.getUsername(), + password: container.getPassword(), + }); + await neo4j.onModuleInit(); + await neo4j.run("CREATE CONSTRAINT node_id_unique IF NOT EXISTS FOR (n:Node) REQUIRE n.id IS UNIQUE"); + await neo4j.run("CREATE INDEX node_project_idx IF NOT EXISTS FOR (n:Node) ON (n.projectId)"); + repo = new NodesRepository(neo4j); + }, 120_000); + + afterAll(async () => { + await neo4j.onModuleDestroy(); + await container.stop(); + }); + + beforeEach(async () => { + await neo4j.run("MATCH (n:Node) DETACH DELETE n"); + }); + + it("create + getById ile node'u geri okur", async () => { + await repo.create(nodeFixture()); + const got = await repo.getById(projectId, "550e8400-e29b-41d4-a716-446655440000"); + expect(got?.type).toBe("Table"); + expect(got?.properties).toEqual({ TableName: "users", Description: "u", Columns: [], Indexes: [] }); + }); + + it("getById yoksa null döner", async () => { + const got = await repo.getById(projectId, "00000000-0000-0000-0000-000000000000"); + expect(got).toBeNull(); + }); + + it("list project'in tüm node'larını döner", async () => { + await repo.create(nodeFixture({ id: "550e8400-e29b-41d4-a716-446655440002" })); + await repo.create(nodeFixture({ id: "550e8400-e29b-41d4-a716-446655440003", type: "DTO", properties: { Name: "X", Description: "d", Fields: [] } })); + const list = await repo.list(projectId); + expect(list).toHaveLength(2); + }); + + it("list type filter çalışıyor", async () => { + await repo.create(nodeFixture({ id: "550e8400-e29b-41d4-a716-446655440002" })); + await repo.create(nodeFixture({ id: "550e8400-e29b-41d4-a716-446655440003", type: "DTO", properties: { Name: "X", Description: "d", Fields: [] } })); + const list = await repo.list(projectId, "Table"); + expect(list).toHaveLength(1); + expect(list[0].type).toBe("Table"); + }); + + it("update position ve properties replace eder", async () => { + await repo.create(nodeFixture()); + await repo.update(projectId, "550e8400-e29b-41d4-a716-446655440000", { + positionX: 999, + positionY: 888, + properties: { TableName: "renamed", Description: "x", Columns: [], Indexes: [] }, + updatedAt: "2026-05-21T11:00:00.000Z", + }); + const got = await repo.getById(projectId, "550e8400-e29b-41d4-a716-446655440000"); + expect(got?.positionX).toBe(999); + expect(got?.properties.TableName).toBe("renamed"); + expect(got?.updatedAt).toBe("2026-05-21T11:00:00.000Z"); + }); + + it("delete silinen node'u getById null döndürür", async () => { + await repo.create(nodeFixture()); + await repo.delete(projectId, "550e8400-e29b-41d4-a716-446655440000"); + const got = await repo.getById(projectId, "550e8400-e29b-41d4-a716-446655440000"); + expect(got).toBeNull(); + }); + + it("findByName proje içi unique check için kullanılır", async () => { + await repo.create(nodeFixture()); + const found = await repo.findByName(projectId, "users"); + expect(found?.id).toBe("550e8400-e29b-41d4-a716-446655440000"); + const notFound = await repo.findByName(projectId, "ghost"); + expect(notFound).toBeNull(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/nodes.repository.spec.ts` +Expected: "Cannot find module './nodes.repository'" hatası. + +- [ ] **Step 3: nodes.repository.ts yaz** + +```ts +import { Injectable } from "@nestjs/common"; +import { Neo4jService } from "../neo4j/neo4j.service"; +import type { NodeKind } from "./schemas"; + +export interface StoredNode { + id: string; + type: NodeKind; + projectId: string; + positionX: number; + positionY: number; + createdAt: string; + updatedAt: string; + properties: Record; +} + +export interface NodeUpdate { + positionX?: number; + positionY?: number; + properties?: Record; + updatedAt: string; +} + +const NAME_KEYS_BY_KIND: Record = { + Table: "TableName", + DTO: "Name", + Model: "ClassName", + Enum: "Name", + View: "ViewName", +}; + +@Injectable() +export class NodesRepository { + constructor(private readonly neo4j: Neo4jService) {} + + async create(node: StoredNode): Promise { + const cypher = ` + CREATE (n:Node:${node.type} { + id: $id, projectId: $projectId, + positionX: $positionX, positionY: $positionY, + createdAt: datetime($createdAt), updatedAt: datetime($updatedAt), + properties: $properties + }) + `; + await this.neo4j.run(cypher, { + id: node.id, + projectId: node.projectId, + positionX: node.positionX, + positionY: node.positionY, + createdAt: node.createdAt, + updatedAt: node.updatedAt, + properties: JSON.stringify(node.properties), + }); + } + + async getById(projectId: string, id: string): Promise { + const result = await this.neo4j.run( + `MATCH (n:Node {id: $id, projectId: $projectId}) RETURN n, labels(n) AS labels`, + { id, projectId }, + ); + if (result.records.length === 0) return null; + return toStoredNode(result.records[0].get("n"), result.records[0].get("labels")); + } + + async list(projectId: string, kind?: NodeKind): Promise { + const cypher = kind + ? `MATCH (n:Node:${kind} {projectId: $projectId}) RETURN n, labels(n) AS labels` + : `MATCH (n:Node {projectId: $projectId}) RETURN n, labels(n) AS labels`; + const result = await this.neo4j.run(cypher, { projectId }); + return result.records.map((r) => toStoredNode(r.get("n"), r.get("labels"))); + } + + async update(projectId: string, id: string, update: NodeUpdate): Promise { + const partial: Record = { updatedAt: update.updatedAt }; + if (update.positionX !== undefined) partial.positionX = update.positionX; + if (update.positionY !== undefined) partial.positionY = update.positionY; + if (update.properties !== undefined) partial.properties = JSON.stringify(update.properties); + + const result = await this.neo4j.run( + `MATCH (n:Node {id: $id, projectId: $projectId}) + SET n += $partial, n.updatedAt = datetime($updatedAt) + RETURN n, labels(n) AS labels`, + { id, projectId, partial, updatedAt: update.updatedAt }, + ); + if (result.records.length === 0) return null; + return toStoredNode(result.records[0].get("n"), result.records[0].get("labels")); + } + + async delete(projectId: string, id: string): Promise { + const result = await this.neo4j.run( + `MATCH (n:Node {id: $id, projectId: $projectId}) + WITH n + DETACH DELETE n + RETURN 1 AS deleted`, + { id, projectId }, + ); + return result.records.length > 0; + } + + async findByName(projectId: string, name: string): Promise { + const result = await this.neo4j.run( + `MATCH (n:Node {projectId: $projectId}) + WHERE apoc.convert.fromJsonMap(n.properties).TableName = $name + OR apoc.convert.fromJsonMap(n.properties).Name = $name + OR apoc.convert.fromJsonMap(n.properties).ClassName = $name + OR apoc.convert.fromJsonMap(n.properties).ViewName = $name + RETURN n, labels(n) AS labels LIMIT 1`, + { projectId, name }, + ); + if (result.records.length === 0) return null; + return toStoredNode(result.records[0].get("n"), result.records[0].get("labels")); + } + + findNameKey(kind: NodeKind): string { + return NAME_KEYS_BY_KIND[kind]; + } +} + +function toStoredNode(n: any, labels: string[]): StoredNode { + const props = n.properties; + const kind = labels.find((l: string) => l !== "Node") as NodeKind; + return { + id: props.id, + type: kind, + projectId: props.projectId, + positionX: Number(props.positionX), + positionY: Number(props.positionY), + createdAt: new Date(props.createdAt).toISOString(), + updatedAt: new Date(props.updatedAt).toISOString(), + properties: JSON.parse(props.properties), + }; +} +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/nodes.repository.spec.ts` +Expected: 7 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/nodes.repository.ts src/nodes/nodes.repository.spec.ts +git commit -m "feat(nodes): NodesRepository — Cypher CRUD + findByName + +properties JSON string (Neo4j map array-of-object index'lenmediği için). +findByName APOC fromJsonMap ile *Name varyantlarını arar — service +unique-name kontrolünü kullanacak. toStoredNode ISO datetime + JSON parse." +``` + +--- + +## Task 16: NodesService — business logic + unique-name + id/timestamp defaults + +**Files:** +- Create: `src/nodes/nodes.service.ts` +- Create: `src/nodes/nodes.service.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/nodes.service.spec.ts +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { ConflictException, NotFoundException, BadRequestException } from "@nestjs/common"; +import { NodesService } from "./nodes.service"; +import type { StoredNode } from "./nodes.repository"; + +function makeRepo(initial: StoredNode[] = []) { + const store = new Map(initial.map((n) => [n.id, n])); + return { + create: vi.fn(async (n: StoredNode) => { store.set(n.id, n); }), + getById: vi.fn(async (_p: string, id: string) => store.get(id) ?? null), + list: vi.fn(async (p: string, k?: string) => Array.from(store.values()).filter((n) => n.projectId === p && (!k || n.type === k))), + update: vi.fn(async (p: string, id: string, upd: any) => { + const existing = store.get(id); + if (!existing) return null; + const next = { ...existing, ...upd }; + if (upd.properties) next.properties = upd.properties; + store.set(id, next); + return next; + }), + delete: vi.fn(async (_p: string, id: string) => store.delete(id)), + findByName: vi.fn(async (p: string, name: string) => { + for (const n of store.values()) { + if (n.projectId !== p) continue; + const props = n.properties as Record; + if (props.TableName === name || props.Name === name || props.ClassName === name || props.ViewName === name) return n; + } + return null; + }), + findNameKey: vi.fn((kind: string) => kind === "Table" ? "TableName" : kind === "Model" ? "ClassName" : kind === "View" ? "ViewName" : "Name"), + }; +} + +const projectId = "550e8400-e29b-41d4-a716-446655440001"; +const validTable = { + id: "550e8400-e29b-41d4-a716-446655440000", + type: "Table" as const, + projectId, + position: { x: 0, y: 0 }, + createdAt: "2026-05-21T10:30:00.000Z", + updatedAt: "2026-05-21T10:30:00.000Z", + properties: { TableName: "users", Description: "u", Columns: [{ Name: "id", DataType: "UUID" as const, IsPrimaryKey: true, IsForeignKey: false, IsNotNull: true, IsUnique: true, AutoIncrement: false }], Indexes: [] }, +}; + +describe("NodesService.create", () => { + let repo: ReturnType; + let service: NodesService; + + beforeEach(() => { + repo = makeRepo(); + service = new NodesService(repo as any); + }); + + it("URL projectId ile body projectId uyuşmuyorsa BadRequestException fırlatır", async () => { + await expect(service.create("other-project", validTable as any)).rejects.toBeInstanceOf(BadRequestException); + }); + + it("id verilmediyse server üretir", async () => { + const { id, ...noId } = validTable; + const result = await service.create(projectId, noId as any); + expect(result.id).toMatch(/^[0-9a-f-]{36}$/); + }); + + it("createdAt/updatedAt verilmediyse server üretir", async () => { + const { createdAt, updatedAt, ...rest } = validTable; + const result = await service.create(projectId, rest as any); + expect(result.createdAt).toBeDefined(); + expect(result.updatedAt).toBeDefined(); + }); + + it("aynı id zaten varsa ERR_ID_CONFLICT", async () => { + repo = makeRepo([{ id: validTable.id, type: "Table", projectId, positionX: 0, positionY: 0, createdAt: "x", updatedAt: "x", properties: {} }]); + service = new NodesService(repo as any); + await expect(service.create(projectId, validTable as any)) + .rejects.toMatchObject({ response: { code: "ERR_ID_CONFLICT" } }); + }); + + it("aynı isim varsa ERR_NAME_DUPLICATE", async () => { + repo = makeRepo([{ id: "x", type: "Table", projectId, positionX: 0, positionY: 0, createdAt: "x", updatedAt: "x", properties: { TableName: "users" } }]); + service = new NodesService(repo as any); + const { id, ...noId } = validTable; + await expect(service.create(projectId, noId as any)) + .rejects.toMatchObject({ response: { code: "ERR_NAME_DUPLICATE" } }); + }); +}); + +describe("NodesService.update", () => { + let repo: ReturnType; + let service: NodesService; + + beforeEach(() => { + repo = makeRepo([{ ...validTable, positionX: 0, positionY: 0 } as any]); + service = new NodesService(repo as any); + }); + + it("yok ise NotFoundException", async () => { + await expect(service.update(projectId, "00000000-0000-0000-0000-000000000000", { position: { x: 1, y: 1 } })) + .rejects.toBeInstanceOf(NotFoundException); + }); + + it("type değiştirmeye çalışırsa ERR_KIND_IMMUTABLE", async () => { + await expect(service.update(projectId, validTable.id, { type: "DTO" } as any)) + .rejects.toMatchObject({ response: { code: "ERR_KIND_IMMUTABLE" } }); + }); + + it("position update updatedAt'i de set eder", async () => { + const result = await service.update(projectId, validTable.id, { position: { x: 99, y: 88 } }); + expect(result.position.x).toBe(99); + expect(result.updatedAt).not.toBe(validTable.updatedAt); + }); +}); + +describe("NodesService.delete", () => { + it("yok ise NotFoundException", async () => { + const repo = makeRepo(); + const service = new NodesService(repo as any); + await expect(service.delete(projectId, "00000000-0000-0000-0000-000000000000")) + .rejects.toBeInstanceOf(NotFoundException); + }); + + it("var ise siler", async () => { + const repo = makeRepo([{ ...validTable, positionX: 0, positionY: 0 } as any]); + const service = new NodesService(repo as any); + await expect(service.delete(projectId, validTable.id)).resolves.toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/nodes.service.spec.ts` +Expected: "Cannot find module './nodes.service'" hatası. + +- [ ] **Step 3: nodes.service.ts yaz** + +```ts +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { randomUUID } from "node:crypto"; +import type { Node, NodeKind } from "./schemas"; +import { NodesRepository, type StoredNode } from "./nodes.repository"; + +type CreateInput = Omit & { + id?: string; + createdAt?: string; + updatedAt?: string; +}; + +export interface UpdateInput { + position?: { x: number; y: number }; + properties?: Record; + type?: NodeKind; // varsa rejected +} + +@Injectable() +export class NodesService { + constructor(private readonly repo: NodesRepository) {} + + async create(urlProjectId: string, input: CreateInput): Promise { + if (input.projectId !== urlProjectId) { + throw new BadRequestException({ + code: "ERR_PROJECT_MISMATCH", + message: "URL'deki projectId ile body'deki projectId uyuşmuyor.", + }); + } + + const id = input.id ?? randomUUID(); + const now = new Date().toISOString(); + const createdAt = input.createdAt ?? now; + const updatedAt = input.updatedAt ?? now; + + if (input.id) { + const existing = await this.repo.getById(urlProjectId, input.id); + if (existing) { + throw new ConflictException({ + code: "ERR_ID_CONFLICT", + message: `id '${input.id}' zaten kullanılıyor.`, + }); + } + } + + const nameKey = this.repo.findNameKey(input.type); + const name = (input.properties as Record)[nameKey] as string | undefined; + if (name) { + const collision = await this.repo.findByName(urlProjectId, name); + if (collision) { + throw new ConflictException({ + code: "ERR_NAME_DUPLICATE", + message: `'${name}' adı bu projede zaten kullanılıyor.`, + }); + } + } + + const stored: StoredNode = { + id, + type: input.type, + projectId: urlProjectId, + positionX: input.position.x, + positionY: input.position.y, + createdAt, + updatedAt, + properties: input.properties as Record, + }; + await this.repo.create(stored); + return this.toNode(stored); + } + + async getById(projectId: string, id: string): Promise { + const stored = await this.repo.getById(projectId, id); + if (!stored) { + throw new NotFoundException({ + code: "ERR_NODE_NOT_FOUND", + message: `Node '${id}' bulunamadı.`, + }); + } + return this.toNode(stored); + } + + async list(projectId: string, kind?: NodeKind): Promise { + const stored = await this.repo.list(projectId, kind); + return stored.map((s) => this.toNode(s)); + } + + async update(projectId: string, id: string, input: UpdateInput): Promise { + if (input.type !== undefined) { + throw new BadRequestException({ + code: "ERR_KIND_IMMUTABLE", + message: "Node tipi (type) değiştirilemez.", + }); + } + const existing = await this.repo.getById(projectId, id); + if (!existing) { + throw new NotFoundException({ + code: "ERR_NODE_NOT_FOUND", + message: `Node '${id}' bulunamadı.`, + }); + } + + // Unique-name kontrolü (properties replace ediliyorsa) + if (input.properties) { + const nameKey = this.repo.findNameKey(existing.type); + const newName = input.properties[nameKey] as string | undefined; + const oldName = (existing.properties as Record)[nameKey] as string | undefined; + if (newName && newName !== oldName) { + const collision = await this.repo.findByName(projectId, newName); + if (collision && collision.id !== id) { + throw new ConflictException({ + code: "ERR_NAME_DUPLICATE", + message: `'${newName}' adı bu projede zaten kullanılıyor.`, + }); + } + } + } + + const updatedAt = new Date().toISOString(); + const updated = await this.repo.update(projectId, id, { + positionX: input.position?.x, + positionY: input.position?.y, + properties: input.properties, + updatedAt, + }); + if (!updated) { + throw new NotFoundException({ + code: "ERR_NODE_NOT_FOUND", + message: `Node '${id}' bulunamadı.`, + }); + } + return this.toNode(updated); + } + + async delete(projectId: string, id: string): Promise { + const deleted = await this.repo.delete(projectId, id); + if (!deleted) { + throw new NotFoundException({ + code: "ERR_NODE_NOT_FOUND", + message: `Node '${id}' bulunamadı.`, + }); + } + } + + private toNode(s: StoredNode): Node { + return { + id: s.id, + type: s.type, + projectId: s.projectId, + position: { x: s.positionX, y: s.positionY }, + createdAt: s.createdAt, + updatedAt: s.updatedAt, + properties: s.properties, + } as Node; + } +} +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/nodes.service.spec.ts` +Expected: 9 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/nodes.service.ts src/nodes/nodes.service.spec.ts +git commit -m "feat(nodes): NodesService — id/timestamp defaults + unique-name + immutability + +URL/body projectId mismatch → ERR_PROJECT_MISMATCH (400). +id varsa zaten kullanılıyor mu → ERR_ID_CONFLICT (409). +*Name proje içi unique → ERR_NAME_DUPLICATE (409). +PATCH type değiştirmeye çalışırsa → ERR_KIND_IMMUTABLE (400). +Yok ise → ERR_NODE_NOT_FOUND (404)." +``` + +--- + +## Task 17: NodesController — POST endpoint + +**Files:** +- Create: `src/nodes/dto/create-node.dto.ts` +- Create: `src/nodes/dto/node-response.dto.ts` +- Create: `src/nodes/nodes.controller.ts` +- Create: `src/nodes/nodes.module.ts` +- Modify: `src/app.module.ts` + +- [ ] **Step 1: create-node.dto.ts yaz** + +```ts +// src/nodes/dto/create-node.dto.ts +import { z } from "zod"; +import { TableNodeSchema } from "../schemas/table.schema"; +import { DTONodeSchema } from "../schemas/dto.schema"; +import { ModelNodeSchema } from "../schemas/model.schema"; +import { EnumNodeSchema } from "../schemas/enum.schema"; +import { ViewNodeSchema } from "../schemas/view.schema"; + +const makeCreatable = >(schema: S) => + schema.partial({ id: true, createdAt: true, updatedAt: true }); + +export const CreateNodeSchema = z.discriminatedUnion("type", [ + makeCreatable(TableNodeSchema as any), + makeCreatable(DTONodeSchema as any), + makeCreatable(ModelNodeSchema as any), + makeCreatable(EnumNodeSchema as any), + makeCreatable(ViewNodeSchema as any), +]); + +export type CreateNodeInput = z.infer; +``` + +- [ ] **Step 2: node-response.dto.ts yaz** + +```ts +// src/nodes/dto/node-response.dto.ts +import type { Node } from "../schemas"; +import type { SuccessEnvelope } from "../../common/envelope"; + +export type NodeResponse = SuccessEnvelope; +export type NodeListResponse = SuccessEnvelope<{ nodes: Node[]; total: number }>; +``` + +- [ ] **Step 3: Controller POST için failing e2e test yaz** + +```ts +// src/nodes/nodes.controller.spec.ts (controller-level unit test; e2e ayrıca Task 20'de) +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { NodesController } from "./nodes.controller"; +import { NodesService } from "./nodes.service"; + +const projectId = "550e8400-e29b-41d4-a716-446655440001"; +const validTablePayload = { + type: "Table", + projectId, + position: { x: 0, y: 0 }, + properties: { + TableName: "users", + Description: "u", + Columns: [{ Name: "id", DataType: "UUID", IsPrimaryKey: true, IsForeignKey: false, IsNotNull: true, IsUnique: true, AutoIncrement: false }], + Indexes: [], + }, +}; + +describe("NodesController.create", () => { + let service: { create: ReturnType }; + let controller: NodesController; + + beforeEach(() => { + service = { create: vi.fn(async (_p, input) => ({ ...input, id: "x", createdAt: "t", updatedAt: "t" })) }; + controller = new NodesController(service as unknown as NodesService); + }); + + it("service.create'i URL projectId ile çağırır ve envelope döner", async () => { + const result = await controller.create(projectId, validTablePayload as any); + expect(service.create).toHaveBeenCalledWith(projectId, validTablePayload); + expect(result.success).toBe(true); + expect(result.data.id).toBe("x"); + }); +}); +``` + +- [ ] **Step 4: nodes.controller.ts yaz (POST sadece şimdilik)** + +```ts +import { Body, Controller, HttpCode, Param, Post, UsePipes } from "@nestjs/common"; +import { NodesService } from "./nodes.service"; +import { ZodValidationPipe } from "../common/pipes/zod-validation.pipe"; +import { CreateNodeSchema, type CreateNodeInput } from "./dto/create-node.dto"; +import { ok } from "../common/envelope"; +import type { NodeResponse } from "./dto/node-response.dto"; + +@Controller("projects/:projectId/nodes") +export class NodesController { + constructor(private readonly service: NodesService) {} + + @Post() + @HttpCode(201) + @UsePipes(new ZodValidationPipe(CreateNodeSchema)) + async create( + @Param("projectId") projectId: string, + @Body() body: CreateNodeInput, + ): Promise { + const created = await this.service.create(projectId, body as any); + return ok(created); + } +} +``` + +- [ ] **Step 5: nodes.module.ts yaz** + +```ts +import { Module } from "@nestjs/common"; +import { NodesController } from "./nodes.controller"; +import { NodesService } from "./nodes.service"; +import { NodesRepository } from "./nodes.repository"; + +@Module({ + controllers: [NodesController], + providers: [NodesService, NodesRepository], +}) +export class NodesModule {} +``` + +- [ ] **Step 6: app.module.ts güncelle** + +```ts +import { Module } from "@nestjs/common"; +import { Neo4jModule } from "./neo4j/neo4j.module"; +import { NodesModule } from "./nodes/nodes.module"; + +@Module({ + imports: [Neo4jModule, NodesModule], +}) +export class AppModule {} +``` + +- [ ] **Step 7: Controller test geçer** + +Run: `pnpm test src/nodes/nodes.controller.spec.ts` +Expected: 1 test passed. + +- [ ] **Step 8: Build sanity** + +Run: `pnpm build` +Expected: hata yok. + +- [ ] **Step 9: Commit** + +```bash +git add src/nodes/dto/ src/nodes/nodes.controller.ts src/nodes/nodes.module.ts src/nodes/nodes.controller.spec.ts src/app.module.ts +git commit -m "feat(nodes): POST /api/v1/projects/:id/nodes — create endpoint + +CreateNodeSchema base alanları opsiyonel (id/createdAt/updatedAt), kind + +properties + projectId zorunlu. ZodValidationPipe shema'yı uygular, +SchemaErrorFilter hataları ERR_SCHEMA_INVALID envelope'una çevirir. +201 + success envelope plans/API Spec birebir." +``` + +--- + +## Task 18: GET single + GET list + +**Files:** +- Modify: `src/nodes/nodes.controller.ts` + +- [ ] **Step 1: Controller test'ine GET case'leri ekle** + +`src/nodes/nodes.controller.spec.ts` dosyasının `describe` bloklarının yanına ekle: + +```ts +describe("NodesController.getById", () => { + it("service.getById'i çağırır ve envelope döner", async () => { + const service = { getById: vi.fn(async () => ({ id: "x", type: "Table" })) }; + const controller = new NodesController(service as unknown as NodesService); + const result = await controller.getById("p", "x"); + expect(service.getById).toHaveBeenCalledWith("p", "x"); + expect(result.success).toBe(true); + }); +}); + +describe("NodesController.list", () => { + it("type filter ile çağırır", async () => { + const service = { list: vi.fn(async () => [{ id: "x", type: "Table" }]) }; + const controller = new NodesController(service as unknown as NodesService); + const result = await controller.list("p", "Table"); + expect(service.list).toHaveBeenCalledWith("p", "Table"); + expect(result.success).toBe(true); + expect(result.data.total).toBe(1); + }); + + it("type filter olmadan çağırır", async () => { + const service = { list: vi.fn(async () => []) }; + const controller = new NodesController(service as unknown as NodesService); + const result = await controller.list("p", undefined); + expect(service.list).toHaveBeenCalledWith("p", undefined); + expect(result.data.total).toBe(0); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/nodes/nodes.controller.spec.ts` +Expected: "controller.getById is not a function" hatası. + +- [ ] **Step 3: nodes.controller.ts'e GET method'larını ekle** + +`src/nodes/nodes.controller.ts` içeriğini şu hale getir: + +```ts +import { Body, Controller, Get, HttpCode, Param, Post, Query, UsePipes } from "@nestjs/common"; +import { NodesService } from "./nodes.service"; +import { ZodValidationPipe } from "../common/pipes/zod-validation.pipe"; +import { CreateNodeSchema, type CreateNodeInput } from "./dto/create-node.dto"; +import { ok } from "../common/envelope"; +import type { NodeResponse, NodeListResponse } from "./dto/node-response.dto"; +import type { NodeKind } from "./schemas"; + +const KIND_VALUES: NodeKind[] = ["Table", "DTO", "Model", "Enum", "View"]; + +@Controller("projects/:projectId/nodes") +export class NodesController { + constructor(private readonly service: NodesService) {} + + @Post() + @HttpCode(201) + @UsePipes(new ZodValidationPipe(CreateNodeSchema)) + async create( + @Param("projectId") projectId: string, + @Body() body: CreateNodeInput, + ): Promise { + const created = await this.service.create(projectId, body as any); + return ok(created); + } + + @Get(":nodeId") + async getById( + @Param("projectId") projectId: string, + @Param("nodeId") nodeId: string, + ): Promise { + const node = await this.service.getById(projectId, nodeId); + return ok(node); + } + + @Get() + async list( + @Param("projectId") projectId: string, + @Query("type") type: string | undefined, + ): Promise { + const kind = type && KIND_VALUES.includes(type as NodeKind) ? (type as NodeKind) : undefined; + const nodes = await this.service.list(projectId, kind); + return ok({ nodes, total: nodes.length }); + } +} +``` + +- [ ] **Step 4: Test geçer** + +Run: `pnpm test src/nodes/nodes.controller.spec.ts` +Expected: 3 tests passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/nodes.controller.ts src/nodes/nodes.controller.spec.ts +git commit -m "feat(nodes): GET /nodes/:id + GET /nodes?type — list with filter + +GET tek node envelope döner; yok ise NotFoundFilter ERR_NODE_NOT_FOUND. +GET list opsiyonel ?type filter, KIND_VALUES whitelist (geçersiz tip +ignore edilir, tüm node'lar döner)." +``` + +--- + +## Task 19: PATCH + DELETE + +**Files:** +- Create: `src/nodes/dto/update-node.dto.ts` +- Modify: `src/nodes/nodes.controller.ts` + +- [ ] **Step 1: update-node.dto.ts yaz** + +```ts +// src/nodes/dto/update-node.dto.ts +import { z } from "zod"; +import { PositionSchema } from "../schemas/base.schema"; + +export const UpdateNodeSchema = z.object({ + position: PositionSchema.optional(), + properties: z.record(z.unknown()).optional(), + // type yasak — Zod tarafında bilinmeyen alan; ama açık reject için izleriz + type: z.never().optional(), +}).strict(); + +export type UpdateNodeInput = z.infer; +``` + +- [ ] **Step 2: Controller PATCH+DELETE test'leri ekle** + +`src/nodes/nodes.controller.spec.ts`'e ekle: + +```ts +describe("NodesController.update", () => { + it("position update", async () => { + const service = { update: vi.fn(async () => ({ id: "x", type: "Table" })) }; + const controller = new NodesController(service as unknown as NodesService); + const result = await controller.update("p", "x", { position: { x: 1, y: 2 } } as any); + expect(service.update).toHaveBeenCalledWith("p", "x", { position: { x: 1, y: 2 } }); + expect(result.success).toBe(true); + }); +}); + +describe("NodesController.delete", () => { + it("service.delete'i çağırır", async () => { + const service = { delete: vi.fn(async () => undefined) }; + const controller = new NodesController(service as unknown as NodesService); + await controller.delete("p", "x"); + expect(service.delete).toHaveBeenCalledWith("p", "x"); + }); +}); +``` + +- [ ] **Step 3: Test fail eder** + +Run: `pnpm test src/nodes/nodes.controller.spec.ts` +Expected: "controller.update is not a function" hatası. + +- [ ] **Step 4: nodes.controller.ts'e PATCH+DELETE ekle** + +`src/nodes/nodes.controller.ts` içeriğini şu hale getir: + +```ts +import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Query, UsePipes } from "@nestjs/common"; +import { NodesService } from "./nodes.service"; +import { ZodValidationPipe } from "../common/pipes/zod-validation.pipe"; +import { CreateNodeSchema, type CreateNodeInput } from "./dto/create-node.dto"; +import { UpdateNodeSchema, type UpdateNodeInput } from "./dto/update-node.dto"; +import { ok } from "../common/envelope"; +import type { NodeResponse, NodeListResponse } from "./dto/node-response.dto"; +import type { NodeKind } from "./schemas"; + +const KIND_VALUES: NodeKind[] = ["Table", "DTO", "Model", "Enum", "View"]; + +@Controller("projects/:projectId/nodes") +export class NodesController { + constructor(private readonly service: NodesService) {} + + @Post() + @HttpCode(201) + @UsePipes(new ZodValidationPipe(CreateNodeSchema)) + async create( + @Param("projectId") projectId: string, + @Body() body: CreateNodeInput, + ): Promise { + const created = await this.service.create(projectId, body as any); + return ok(created); + } + + @Get(":nodeId") + async getById( + @Param("projectId") projectId: string, + @Param("nodeId") nodeId: string, + ): Promise { + const node = await this.service.getById(projectId, nodeId); + return ok(node); + } + + @Get() + async list( + @Param("projectId") projectId: string, + @Query("type") type: string | undefined, + ): Promise { + const kind = type && KIND_VALUES.includes(type as NodeKind) ? (type as NodeKind) : undefined; + const nodes = await this.service.list(projectId, kind); + return ok({ nodes, total: nodes.length }); + } + + @Patch(":nodeId") + @UsePipes(new ZodValidationPipe(UpdateNodeSchema)) + async update( + @Param("projectId") projectId: string, + @Param("nodeId") nodeId: string, + @Body() body: UpdateNodeInput, + ): Promise { + const updated = await this.service.update(projectId, nodeId, body as any); + return ok(updated); + } + + @Delete(":nodeId") + @HttpCode(204) + async delete( + @Param("projectId") projectId: string, + @Param("nodeId") nodeId: string, + ): Promise { + await this.service.delete(projectId, nodeId); + } +} +``` + +- [ ] **Step 5: Test geçer** + +Run: `pnpm test src/nodes/nodes.controller.spec.ts` +Expected: 5 tests passed (önceki 3 + yeni 2). + +- [ ] **Step 6: Tüm src/ testleri geçiyor mu** + +Run: `pnpm test` +Expected: tüm testler geçer. + +- [ ] **Step 7: Commit** + +```bash +git add src/nodes/dto/update-node.dto.ts src/nodes/nodes.controller.ts src/nodes/nodes.controller.spec.ts +git commit -m "feat(nodes): PATCH + DELETE endpoint'leri — field-level replace + +PATCH body'sinde position ve/veya properties opsiyonel; type field'ı +z.never() ile reddedilir (ERR_KIND_IMMUTABLE service tarafında). +DELETE 204 idempotent." +``` + +--- + +# Sprint 5 — E2E ve Polish + +## Task 20: E2E test — 5 kind tam CRUD round-trip + error paths + +**Files:** +- Create: `vitest.e2e.config.ts` +- Create: `test/nodes.e2e-spec.ts` + +- [ ] **Step 1: vitest.e2e.config.ts yaz** + +```ts +import { defineConfig } from "vitest/config"; +import path from "node:path"; + +export default defineConfig({ + test: { + include: ["test/**/*.e2e-spec.ts"], + environment: "node", + globals: false, + testTimeout: 180_000, // Testcontainers ilk image pull için + hookTimeout: 180_000, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); +``` + +- [ ] **Step 2: E2E test yaz** + +```ts +// test/nodes.e2e-spec.ts +import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest"; +import { Test } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import request from "supertest"; +import { Neo4jContainer, StartedNeo4jContainer } from "@testcontainers/neo4j"; +import { NodesModule } from "../src/nodes/nodes.module"; +import { Neo4jModule } from "../src/neo4j/neo4j.module"; +import { Neo4jService } from "../src/neo4j/neo4j.service"; +import { SchemaErrorFilter } from "../src/common/filters/schema-error.filter"; +import { NotFoundFilter } from "../src/common/filters/not-found.filter"; +import { ConflictFilter } from "../src/common/filters/conflict.filter"; +import { InternalFilter } from "../src/common/filters/internal.filter"; + +const projectId = "550e8400-e29b-41d4-a716-446655440001"; + +describe("Nodes E2E", () => { + let container: StartedNeo4jContainer; + let app: INestApplication; + let neo4j: Neo4jService; + + beforeAll(async () => { + container = await new Neo4jContainer("neo4j:5-community").withApoc().start(); + + // Env'i container'a göre set et + process.env.NEO4J_URI = container.getBoltUri(); + process.env.NEO4J_USER = container.getUsername(); + process.env.NEO4J_PASSWORD = container.getPassword(); + process.env.PORT = "0"; + process.env.CORS_ORIGIN = "http://localhost:3000"; + + const moduleRef = await Test.createTestingModule({ + imports: [Neo4jModule, NodesModule], + }).compile(); + + app = moduleRef.createNestApplication(); + app.setGlobalPrefix("api/v1"); + app.useGlobalFilters( + new InternalFilter(), + new ConflictFilter(), + new NotFoundFilter(), + new SchemaErrorFilter(), + ); + await app.init(); + + neo4j = app.get(Neo4jService); + await neo4j.run("CREATE CONSTRAINT node_id_unique IF NOT EXISTS FOR (n:Node) REQUIRE n.id IS UNIQUE"); + await neo4j.run("CREATE INDEX node_project_idx IF NOT EXISTS FOR (n:Node) ON (n.projectId)"); + }, 180_000); + + afterAll(async () => { + await app.close(); + await container.stop(); + }); + + beforeEach(async () => { + await neo4j.run("MATCH (n:Node) DETACH DELETE n"); + }); + + const fixtures = { + Table: { + type: "Table" as const, + projectId, + position: { x: 0, y: 0 }, + properties: { + TableName: "users", + Description: "u", + Columns: [{ Name: "id", DataType: "UUID", IsPrimaryKey: true, IsForeignKey: false, IsNotNull: true, IsUnique: true, AutoIncrement: false }], + Indexes: [], + }, + }, + DTO: { + type: "DTO" as const, + projectId, + position: { x: 0, y: 0 }, + properties: { + Name: "CreateUserDTO", + Description: "d", + Fields: [{ Name: "email", DataType: "string", IsRequired: true, IsArray: false }], + }, + }, + Model: { + type: "Model" as const, + projectId, + position: { x: 0, y: 0 }, + properties: { + ClassName: "User", + Description: "m", + Properties: [{ Name: "id", Type: "UUID" }], + Methods: [], + }, + }, + Enum: { + type: "Enum" as const, + projectId, + position: { x: 0, y: 0 }, + properties: { Name: "OrderStatus", Description: "e", Values: ["PENDING", "SHIPPED"] }, + }, + View: { + type: "View" as const, + projectId, + position: { x: 0, y: 0 }, + properties: { ViewName: "active_users", Description: "v", Definition: "SELECT 1", SourceTables: ["users"], Materialized: false }, + }, + }; + + for (const [kind, payload] of Object.entries(fixtures)) { + it(`${kind}: full CRUD round-trip`, async () => { + // POST + const created = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/nodes`) + .send(payload) + .expect(201); + expect(created.body.success).toBe(true); + const id = created.body.data.id; + expect(id).toMatch(/^[0-9a-f-]{36}$/); + + // GET single + const got = await request(app.getHttpServer()) + .get(`/api/v1/projects/${projectId}/nodes/${id}`) + .expect(200); + expect(got.body.data.type).toBe(kind); + + // GET list ?type= + const listed = await request(app.getHttpServer()) + .get(`/api/v1/projects/${projectId}/nodes?type=${kind}`) + .expect(200); + expect(listed.body.data.total).toBe(1); + + // PATCH position + const patched = await request(app.getHttpServer()) + .patch(`/api/v1/projects/${projectId}/nodes/${id}`) + .send({ position: { x: 999, y: 888 } }) + .expect(200); + expect(patched.body.data.position).toEqual({ x: 999, y: 888 }); + + // DELETE + await request(app.getHttpServer()) + .delete(`/api/v1/projects/${projectId}/nodes/${id}`) + .expect(204); + + // GET → 404 + const notFound = await request(app.getHttpServer()) + .get(`/api/v1/projects/${projectId}/nodes/${id}`) + .expect(404); + expect(notFound.body.error.code).toBe("ERR_NODE_NOT_FOUND"); + }); + } + + it("ERR_SCHEMA_INVALID — Description eksik", async () => { + const payload = JSON.parse(JSON.stringify(fixtures.Table)); + delete payload.properties.Description; + const res = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/nodes`) + .send(payload) + .expect(400); + expect(res.body.error.code).toBe("ERR_SCHEMA_INVALID"); + expect(res.body.error.details).toBeDefined(); + }); + + it("ERR_PROJECT_MISMATCH — URL ile body uyuşmuyor", async () => { + const res = await request(app.getHttpServer()) + .post(`/api/v1/projects/different-project/nodes`) + .send(fixtures.Table) + .expect(400); + expect(res.body.error.code).toBe("ERR_PROJECT_MISMATCH"); + }); + + it("ERR_NAME_DUPLICATE — aynı TableName ikinci kez", async () => { + await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/nodes`) + .send(fixtures.Table) + .expect(201); + const res = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/nodes`) + .send(fixtures.Table) + .expect(409); + expect(res.body.error.code).toBe("ERR_NAME_DUPLICATE"); + }); + + it("ERR_KIND_IMMUTABLE — PATCH type değiştirmeye çalışırsa", async () => { + const created = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/nodes`) + .send(fixtures.Table); + const res = await request(app.getHttpServer()) + .patch(`/api/v1/projects/${projectId}/nodes/${created.body.data.id}`) + .send({ type: "DTO" }) + .expect(400); + expect(["ERR_KIND_IMMUTABLE", "ERR_SCHEMA_INVALID"]).toContain(res.body.error.code); + }); +}); +``` + +- [ ] **Step 3: E2E test'i çalıştır** + +Run: `pnpm test:e2e` +Expected: 9 tests passed (5 kind × CRUD + 4 error path). İlk run ~2-3 dakika (Docker image pull). + +- [ ] **Step 4: Commit** + +```bash +git add vitest.e2e.config.ts test/nodes.e2e-spec.ts +git commit -m "test(e2e): 5 kind CRUD round-trip + 4 error path + +Testcontainers Neo4j + supertest. Her kind: POST 201 → GET 200 → +GET list ?type filter → PATCH 200 → DELETE 204 → GET 404. Error path'lar: +ERR_SCHEMA_INVALID, ERR_PROJECT_MISMATCH, ERR_NAME_DUPLICATE, +ERR_KIND_IMMUTABLE." +``` + +--- + +## Task 21: Health endpoint + README update + +**Files:** +- Create: `src/health/health.controller.ts` +- Create: `src/health/health.controller.spec.ts` +- Modify: `src/app.module.ts` +- Modify: `README.md` + +- [ ] **Step 1: Health test yaz** + +```ts +// src/health/health.controller.spec.ts +import { describe, it, expect } from "vitest"; +import { HealthController } from "./health.controller"; + +describe("HealthController", () => { + it("status: ok döner", () => { + const controller = new HealthController(); + const result = controller.check(); + expect(result.success).toBe(true); + expect(result.data.status).toBe("ok"); + expect(typeof result.data.uptime).toBe("number"); + }); +}); +``` + +- [ ] **Step 2: Test fail eder** + +Run: `pnpm test src/health/health.controller.spec.ts` +Expected: "Cannot find module './health.controller'" hatası. + +- [ ] **Step 3: health.controller.ts yaz** + +```ts +import { Controller, Get } from "@nestjs/common"; +import { ok } from "../common/envelope"; + +@Controller("health") +export class HealthController { + @Get() + check() { + return ok({ status: "ok", uptime: process.uptime() }); + } +} +``` + +- [ ] **Step 4: app.module.ts güncelle** + +```ts +import { Module } from "@nestjs/common"; +import { Neo4jModule } from "./neo4j/neo4j.module"; +import { NodesModule } from "./nodes/nodes.module"; +import { HealthController } from "./health/health.controller"; + +@Module({ + imports: [Neo4jModule, NodesModule], + controllers: [HealthController], +}) +export class AppModule {} +``` + +- [ ] **Step 5: Test geçer** + +Run: `pnpm test src/health/health.controller.spec.ts` +Expected: 1 test passed. + +- [ ] **Step 6: README'yi son haliyle güncelle** + +`README.md` içeriğini şu hale getir: + +```markdown +# solarch-backend + +Solarch'ın mimari graf backend'i. Solarch core (`Solarch/`) repo'sundan +**bağımsız** olarak geliştirilir. Sadece Node CRUD + şema doğrulamayla +başlar; Edge, Rules Engine, AI batch ve kod üretim motoru sonraki fazlara +bırakılmıştır. + +## Stack + +- **NestJS 11** — modüler yapı, DI, decorator disiplini +- **Zod 3** — discriminated union + JSON Schema export +- **Neo4j 5** (community) — graph DB; ileride Rules Engine için bedava + graph traversal +- **TypeScript 5** + **pnpm 10** + **Vitest 2** + **Testcontainers** + +## Phase 1 — Node CRUD (bu repo'nun şu anki kapsamı) + +Sadece **Veri ailesi** node tipleri: + +- `Table`, `DTO`, `Model`, `Enum`, `View` + +API yüzeyi (5 endpoint): + +| Method | Path | +|---|---| +| POST | `/api/v1/projects/:projectId/nodes` | +| GET | `/api/v1/projects/:projectId/nodes` (`?type=Table` opsiyonel) | +| GET | `/api/v1/projects/:projectId/nodes/:nodeId` | +| PATCH | `/api/v1/projects/:projectId/nodes/:nodeId` | +| DELETE | `/api/v1/projects/:projectId/nodes/:nodeId` | +| GET | `/api/v1/health` | + +Tam tasarım: [`docs/specs/2026-05-21-node-types-design.md`](docs/specs/2026-05-21-node-types-design.md) +Implementation planı: [`docs/plans/2026-05-21-node-types-implementation.md`](docs/plans/2026-05-21-node-types-implementation.md) + +## Geliştirme + +```bash +# 1. Bağımlılıklar +pnpm install + +# 2. Neo4j'i ayağa kaldır +pnpm neo4j:up + +# 3. Constraints + index +cp .env.example .env +pnpm neo4j:migrate + +# 4. Dev server +pnpm dev # http://localhost:4000/api/v1 + +# 5. Testler +pnpm test # unit +pnpm test:e2e # e2e (Testcontainers — ilk run ~2dk) +``` + +## Yol haritası + +| Faz | Kapsam | Durum | +|---|---|---| +| 1 | Node CRUD + Veri ailesi (5 tip) | ✅ tamamlandı | +| 1.5 | Diğer node aileleri (İş/Erişim/Altyapı/İstemci/Güvenlik/Yapı) | ⏳ | +| 2 | Edge CRUD + Rules Engine (whitelist/blacklist) | ⏳ | +| 2.5 | Conditional rules (circular dep, encapsulation, type mismatch) | ⏳ | +| 3 | AI batch apply (`/graph/apply`) + LangGraph agent loop | ⏳ | +| 4 | Vector DB + GraphRAG ("Chat with Architecture") | ⏳ | +| 5 | Kod üretim motoru (AST scaffold + cerrahi AI) | ⏳ | + +## Yeni Node Tipi Ekleme + +Üç adım (kuralın kendisi — `docs/specs/...` Section 6): + +1. `src/nodes/schemas/.schema.ts` — `BaseNodeSchema.extend({ type: z.literal("Foo"), properties: z.object({...}).strict() }).strict()` +2. `src/nodes/schemas/index.ts` — `NodeSchema` discriminated union'una eklenir + `KIND_LABELS` haritası güncellenir +3. `src/nodes/schemas/.schema.spec.ts` — valid + invalid payload örnekleri + +Union'a girmeyen tip TS compile + Zod runtime'da reddedilir. + +## Lisans + +Henüz lisanslanmadı. +``` + +- [ ] **Step 7: Tüm test paketi yeşil mi** + +Run: `pnpm test && pnpm test:e2e` +Expected: tüm testler geçer. + +- [ ] **Step 8: Build sanity** + +Run: `pnpm build` +Expected: hata yok. + +- [ ] **Step 9: Commit** + +```bash +git add src/health/ src/app.module.ts README.md +git commit -m "feat(health): GET /api/v1/health + README update + +Health endpoint uptime ile basit envelope döner. README'de Phase 1 ✅, +geliştirme komutları, yeni node tipi ekleme adımları net olarak güncel." +``` + +- [ ] **Step 10: Push** + +```bash +git push origin main +``` +Expected: tüm yeni commit'ler origin/main'e gider. + +--- + +## Phase 1 Çıkış Kriterleri Doğrulama + +Spec Section 12 ile karşılaştır: + +- [x] **1.** `solarch-backend` repo'su initialize edilmiş — Task 0 (zaten) +- [x] **2.** NestJS app boot ediyor, `/api/v1/health` 200 dönüyor — Task 1, 21 +- [x] **3.** Docker Compose ile Neo4j ayağa kalkıyor — Task 3 +- [x] **4.** Migration script `node_id_unique` + `node_project_idx` kuruyor — Task 5 +- [x] **5.** 5 Veri ailesi node tipi için tam CRUD endpoint'leri çalışıyor — Task 6-11 (schemas) + 15-19 (CRUD) +- [x] **6.** Tüm endpoint'ler plans/API Spec response envelope formatına uyuyor — Task 12-14 (envelope/filters) +- [x] **7.** Unit + E2E testler geçiyor — Task 20 +- [x] **8.** README + `.env.example` + `docker-compose.yml` mevcut — Task 21, 3 + +--- + +## Notlar + +- **Test sırası önemli:** Sprint 5 (E2E) önce gelmeli ki sonradan ortaya çıkan envelope farklılıkları yakalanabilsin. Sprint 1-4 unit testlere güvenir; E2E mock'lanamayan integration regressionlar için. +- **APOC bağımlılığı:** `findByName` `apoc.convert.fromJsonMap` kullanıyor. Phase 2'de properties struct'a açıldığında APOC'a ihtiyaç kalkar. +- **Testcontainers ilk run:** İlk Neo4j image pull 1-2 dakika. CI'da image'ı cache'lemek isteriz (sonraki sprint). +- **Hot reload:** `pnpm dev` watch mode. `.env` değişirse yeniden başlat. +- **Lock dosyası:** `pnpm-lock.yaml` her commit'te commit edilmeli (deterministik install). diff --git a/apps/server/docs/plans/2026-05-22-node-enrichment-implementation.md b/apps/server/docs/plans/2026-05-22-node-enrichment-implementation.md new file mode 100644 index 0000000..da404a7 --- /dev/null +++ b/apps/server/docs/plans/2026-05-22-node-enrichment-implementation.md @@ -0,0 +1,987 @@ +# Node Properties Enrichment — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 21 node tipinin `properties` Zod şemalarını codegen-ready derinliğe (tam DB/kod modeli) çıkarmak; her yeni alan required, mevcut DB node'ları migration ile dönüştürülür, UI fieldHints + AI prompt güncellenir. + +**Architecture:** Fazlı — Faz A (Veri ailesi: Table/DTO/Model/Enum/View) bu planda tam TDD; Faz B (İş/Erişim) ve Faz C (Altyapı/diğer) yapısal görev başlıkları (kendi tur). Her node: schema enrich → spec güncelle → testler yeşil → fieldHints → commit. Faz sonunda migration + AI prompt + canlı AI doğrulama. + +**Tech Stack:** TypeScript, NestJS 11, Zod 3, neo4j-driver, Vitest 2, Testcontainers, nestjs-zod, LangChain (DeepSeek/Bedrock). + +**Spec:** [`docs/specs/2026-05-22-node-enrichment-design.md`](../specs/2026-05-22-node-enrichment-design.md) + +**Çalışma dizini:** `~/Masaüstü/Arsiv/solarch-backend/` — tüm path'ler ve git komutları buradan. + +--- + +## File Structure + +``` +src/nodes/schemas/ + version.ts ← YENİ: GRAPH_SCHEMA_VERSION + table.schema.ts ← enrich (composite PK/FK, FK actions, check, zengin index) + dto.schema.ts ← enrich (validation rules, nested/enum ref) + model.schema.ts ← enrich (relations, method signatures) + enum.schema.ts ← enrich (key-value, backing type) + view.schema.ts ← enrich (columns, refresh strategy) + *.schema.spec.ts ← her biri yeni alanlar için güncellenir + index.ts ← değişmez (export'lar aynı) +src/nodes/dto/create-node.dto.ts ← Veri ailesi properties shape güncel (otomatik — shape'ten türüyor) +src/node-types/registry.ts ← Veri ailesi fieldHints +src/node-types/node-types.service.ts ← getById response'una fieldHints +src/ai/prompts/system-prompt.ts ← Veri ailesi şema rehberi güncel +src/neo4j/migrations/data/001-enrich-faz-a.ts ← YENİ: mevcut node properties dönüşümü +package.json ← migrate:data script +test/nodes.e2e-spec.ts ← Veri ailesi fixtures güncel +``` + +**Decomposition:** Her node schema dosyası tek sorumluluk (kendi kind'ı). fieldHints registry'de toplu. Migration ayrı script. Her dosya <200 satır hedef. + +--- + +## FAZ A — Veri Ailesi + +### Task 1: GRAPH_SCHEMA_VERSION + +**Files:** +- Create: `src/nodes/schemas/version.ts` +- Test: `src/nodes/schemas/version.spec.ts` + +- [ ] **Step 1: Failing test yaz** + +```ts +// src/nodes/schemas/version.spec.ts +import { describe, it, expect } from "vitest"; +import { GRAPH_SCHEMA_VERSION } from "./version"; + +describe("GRAPH_SCHEMA_VERSION", () => { + it("pozitif integer", () => { + expect(Number.isInteger(GRAPH_SCHEMA_VERSION)).toBe(true); + expect(GRAPH_SCHEMA_VERSION).toBeGreaterThanOrEqual(2); + }); +}); +``` + +- [ ] **Step 2: Test fail** + +Run: `pnpm test src/nodes/schemas/version.spec.ts` +Expected: "Cannot find module './version'" + +- [ ] **Step 3: version.ts yaz** + +```ts +// src/nodes/schemas/version.ts +/** Node properties şema sürümü. Enrichment fazlarında bump edilir. + * v1 = Phase 1 temel şemalar. v2 = Faz A (Veri ailesi codegen-ready). */ +export const GRAPH_SCHEMA_VERSION = 2; +``` + +- [ ] **Step 4: Test pass** + +Run: `pnpm test src/nodes/schemas/version.spec.ts` +Expected: PASS (1 test) + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/version.ts src/nodes/schemas/version.spec.ts +git commit -m "feat(schemas): GRAPH_SCHEMA_VERSION=2 — enrichment versioning" +``` + +--- + +### Task 2: Table schema enrichment + +**Files:** +- Modify: `src/nodes/schemas/table.schema.ts` +- Modify: `src/nodes/schemas/table.schema.spec.ts` + +- [ ] **Step 1: table.schema.ts'i tam DB modeline çıkar** + +`src/nodes/schemas/table.schema.ts` içeriğini şu hale getir: + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const DATA_TYPES = ["INT", "BIGINT", "VARCHAR", "TEXT", "BOOLEAN", "DATETIME", "DATE", "UUID", "FLOAT", "DECIMAL", "JSON", "ENUM"] as const; +const FK_ACTION = ["CASCADE", "RESTRICT", "SET_NULL", "NO_ACTION"] as const; + +const ColumnSchema = z.object({ + Name: z.string().min(1).describe("Kolon adı"), + DataType: z.enum(DATA_TYPES).describe("SQL veri tipi"), + Length: z.number().int().positive().optional().describe("VARCHAR(n) uzunluğu"), + Precision: z.number().int().positive().optional().describe("DECIMAL(p,s) precision"), + Scale: z.number().int().nonnegative().optional().describe("DECIMAL(p,s) scale"), + IsPrimaryKey: z.boolean().describe("Tek-kolon PK"), + IsNotNull: z.boolean().describe("NOT NULL"), + IsUnique: z.boolean().describe("UNIQUE"), + AutoIncrement: z.boolean().describe("AUTO_INCREMENT / SERIAL"), + DefaultValue: z.string().optional().describe("Varsayılan değer ifadesi"), + Comment: z.string().optional().describe("Kolon yorumu"), + EnumRef: z.string().optional().describe("DataType=ENUM ise → Enum node Name"), + IsGenerated: z.boolean().optional().describe("GENERATED kolon"), + GeneratedExpression: z.string().optional().describe("Generated kolon ifadesi"), +}).strict(); + +const ForeignKeySchema = z.object({ + Name: z.string().optional(), + Columns: z.array(z.string().min(1)).min(1), + ReferencesTable: z.string().min(1).describe("Hedef Table Name"), + ReferencesColumns: z.array(z.string().min(1)).min(1), + OnDelete: z.enum(FK_ACTION).default("NO_ACTION"), + OnUpdate: z.enum(FK_ACTION).default("NO_ACTION"), +}).strict(); + +const IndexSchema = z.object({ + IndexName: z.string().min(1), + Columns: z.array(z.string().min(1)).min(1), + Type: z.enum(["BTree", "Hash", "GIN", "GiST"]).default("BTree"), + IsUnique: z.boolean().default(false), + IsPartial: z.boolean().optional(), + WhereClause: z.string().optional(), +}).strict(); + +export const TableNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Table"), + properties: z.object({ + TableName: z.string().min(1), + Description: z.string().min(1), + Columns: z.array(ColumnSchema).min(1), + PrimaryKey: z.object({ Columns: z.array(z.string().min(1)).min(1) }).optional().describe("Composite PK"), + ForeignKeys: z.array(ForeignKeySchema).default([]), + UniqueConstraints: z.array(z.object({ Name: z.string().optional(), Columns: z.array(z.string().min(1)).min(1) })).default([]), + CheckConstraints: z.array(z.object({ Name: z.string().optional(), Expression: z.string().min(1) })).default([]), + Indexes: z.array(IndexSchema).default([]), + }).strict(), +}).strict(); + +export type TableNode = z.infer; +``` + +> NOT: Önceki `Column.IsForeignKey`/`References` kaldırıldı; tüm FK'ler `ForeignKeys[]`'te. `Indexes[]` artık `IsUnique`/`Type` zorunlu default'lu. + +- [ ] **Step 2: table.schema.spec.ts'i güncelle** + +`src/nodes/schemas/table.schema.spec.ts` içeriğini şu hale getir: + +```ts +import { describe, it, expect } from "vitest"; +import { TableNodeSchema } from "./table.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-22T10:30:00.000Z", + updatedAt: "2026-05-22T10:30:00.000Z", +}; + +const validProperties = { + TableName: "users", + Description: "Kullanıcılar", + Columns: [ + { Name: "id", DataType: "UUID", IsPrimaryKey: true, IsNotNull: true, IsUnique: true, AutoIncrement: false }, + { Name: "email", DataType: "VARCHAR", Length: 255, IsPrimaryKey: false, IsNotNull: true, IsUnique: true, AutoIncrement: false }, + { Name: "status", DataType: "ENUM", EnumRef: "UserStatus", IsPrimaryKey: false, IsNotNull: true, IsUnique: false, AutoIncrement: false }, + ], + ForeignKeys: [], + Indexes: [], +}; + +describe("TableNodeSchema (enriched)", () => { + it("geçerli Table'ı parse eder", () => { + const n = TableNodeSchema.parse({ ...validBase, type: "Table", properties: validProperties }); + expect(n.properties.Columns).toHaveLength(3); + expect(n.properties.ForeignKeys).toEqual([]); + }); + + it("composite PrimaryKey kabul eder", () => { + const n = TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, PrimaryKey: { Columns: ["id", "email"] } }, + }); + expect(n.properties.PrimaryKey?.Columns).toEqual(["id", "email"]); + }); + + it("ForeignKey FK actions ile parse eder", () => { + const n = TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { + ...validProperties, + ForeignKeys: [{ Columns: ["org_id"], ReferencesTable: "orgs", ReferencesColumns: ["id"], OnDelete: "CASCADE", OnUpdate: "RESTRICT" }], + }, + }); + expect(n.properties.ForeignKeys[0].OnDelete).toBe("CASCADE"); + }); + + it("CheckConstraint + UniqueConstraint kabul eder", () => { + const n = TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { + ...validProperties, + CheckConstraints: [{ Name: "age_positive", Expression: "age > 0" }], + UniqueConstraints: [{ Name: "uq_email", Columns: ["email"] }], + }, + }); + expect(n.properties.CheckConstraints[0].Expression).toBe("age > 0"); + }); + + it("zengin Index (GIN, partial) parse eder", () => { + const n = TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, Indexes: [{ IndexName: "idx_email", Columns: ["email"], Type: "BTree", IsUnique: true, IsPartial: true, WhereClause: "deleted_at IS NULL" }] }, + }); + expect(n.properties.Indexes[0].IsUnique).toBe(true); + }); + + it("geçersiz DataType reddeder", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, Columns: [{ Name: "x", DataType: "integer", IsPrimaryKey: true, IsNotNull: true, IsUnique: true, AutoIncrement: false }] }, + })).toThrow(); + }); + + it("geçersiz FK action reddeder", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, ForeignKeys: [{ Columns: ["x"], ReferencesTable: "t", ReferencesColumns: ["id"], OnDelete: "DROP", OnUpdate: "NO_ACTION" }] }, + })).toThrow(); + }); + + it("bilinmeyen alan reddeder (strict)", () => { + expect(() => TableNodeSchema.parse({ + ...validBase, type: "Table", + properties: { ...validProperties, Foo: "x" }, + })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test çalıştır** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/nodes/schemas/table.schema.spec.ts` +Expected: PASS (8 tests) + +- [ ] **Step 4: Build sanity** + +Run: `pnpm build` +Expected: hata yok + +- [ ] **Step 5: Commit** + +```bash +git add src/nodes/schemas/table.schema.ts src/nodes/schemas/table.schema.spec.ts +git commit -m "feat(schemas): Table enrichment — composite PK/FK + FK actions + check + rich index" +``` + +--- + +### Task 3: DTO schema enrichment + +**Files:** +- Modify: `src/nodes/schemas/dto.schema.ts` +- Modify: `src/nodes/schemas/dto.schema.spec.ts` + +- [ ] **Step 1: dto.schema.ts'i enrich et** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const VALIDATION_RULES = ["Min", "Max", "MinLength", "MaxLength", "Email", "Url", "Regex", "Pattern", "Positive", "Negative"] as const; + +const ValidationRuleSchema = z.object({ + Rule: z.enum(VALIDATION_RULES), + Value: z.string().optional(), +}).strict(); + +const FieldSchema = z.object({ + Name: z.string().min(1), + DataType: z.string().min(1), + IsRequired: z.boolean(), + IsArray: z.boolean(), + ValidationRules: z.array(ValidationRuleSchema).default([]), + DefaultValue: z.string().optional(), + NestedDTORef: z.string().optional().describe("→ DTO node Name"), + EnumRef: z.string().optional().describe("→ Enum node Name"), + Description: z.string().optional(), +}).strict(); + +export const DTONodeSchema = BaseNodeSchema.extend({ + type: z.literal("DTO"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + Fields: z.array(FieldSchema).min(1), + }).strict(), +}).strict(); + +export type DTONode = z.infer; +``` + +- [ ] **Step 2: dto.schema.spec.ts'i güncelle** + +```ts +import { describe, it, expect } from "vitest"; +import { DTONodeSchema } from "./dto.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-22T10:30:00.000Z", + updatedAt: "2026-05-22T10:30:00.000Z", +}; + +const validProperties = { + Name: "CreateUserDTO", + Description: "Kullanıcı kayıt", + Fields: [ + { Name: "email", DataType: "string", IsRequired: true, IsArray: false, ValidationRules: [{ Rule: "Email" }, { Rule: "MaxLength", Value: "255" }] }, + { Name: "address", DataType: "AddressDTO", IsRequired: false, IsArray: false, NestedDTORef: "AddressDTO" }, + { Name: "role", DataType: "string", IsRequired: true, IsArray: false, EnumRef: "UserRole" }, + ], +}; + +describe("DTONodeSchema (enriched)", () => { + it("geçerli DTO + validation rules parse eder", () => { + const n = DTONodeSchema.parse({ ...validBase, type: "DTO", properties: validProperties }); + expect(n.properties.Fields[0].ValidationRules).toHaveLength(2); + }); + + it("NestedDTORef + EnumRef kabul eder", () => { + const n = DTONodeSchema.parse({ ...validBase, type: "DTO", properties: validProperties }); + expect(n.properties.Fields[1].NestedDTORef).toBe("AddressDTO"); + expect(n.properties.Fields[2].EnumRef).toBe("UserRole"); + }); + + it("ValidationRules default boş array", () => { + const n = DTONodeSchema.parse({ + ...validBase, type: "DTO", + properties: { Name: "X", Description: "d", Fields: [{ Name: "a", DataType: "string", IsRequired: true, IsArray: false }] }, + }); + expect(n.properties.Fields[0].ValidationRules).toEqual([]); + }); + + it("geçersiz validation rule reddeder", () => { + expect(() => DTONodeSchema.parse({ + ...validBase, type: "DTO", + properties: { Name: "X", Description: "d", Fields: [{ Name: "a", DataType: "string", IsRequired: true, IsArray: false, ValidationRules: [{ Rule: "Fancy" }] }] }, + })).toThrow(); + }); + + it("Description zorunlu", () => { + const { Description, ...rest } = validProperties; + expect(() => DTONodeSchema.parse({ ...validBase, type: "DTO", properties: rest })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test çalıştır** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/nodes/schemas/dto.schema.spec.ts` +Expected: PASS (5 tests) + +- [ ] **Step 4: Commit** + +```bash +git add src/nodes/schemas/dto.schema.ts src/nodes/schemas/dto.schema.spec.ts +git commit -m "feat(schemas): DTO enrichment — validation rules + nested/enum ref" +``` + +--- + +### Task 4: Model schema enrichment + +**Files:** +- Modify: `src/nodes/schemas/model.schema.ts` +- Modify: `src/nodes/schemas/model.schema.spec.ts` + +- [ ] **Step 1: model.schema.ts'i enrich et** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +const RELATION = ["OneToOne", "OneToMany", "ManyToOne", "ManyToMany"] as const; + +const PropertySchema = z.object({ + Name: z.string().min(1), + Type: z.string().min(1), + IsNullable: z.boolean().default(false), + IsCollection: z.boolean().default(false), + RelationType: z.enum(RELATION).optional(), + RelatedModelRef: z.string().optional().describe("→ Model node Name"), +}).strict(); + +const MethodSchema = z.object({ + MethodName: z.string().min(1), + Visibility: z.enum(["public", "private", "protected"]).default("public"), + Parameters: z.array(z.object({ + Name: z.string().min(1), + Type: z.string().min(1), + Optional: z.boolean().default(false), + Default: z.string().optional(), + })).default([]), + ReturnType: z.string().min(1), + IsAsync: z.boolean().default(false), + IsStatic: z.boolean().default(false), +}).strict(); + +export const ModelNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Model"), + properties: z.object({ + ClassName: z.string().min(1), + Description: z.string().min(1), + TableRef: z.string().optional().describe("→ Table node Name"), + Properties: z.array(PropertySchema).min(1), + Methods: z.array(MethodSchema).default([]), + }).strict(), +}).strict(); + +export type ModelNode = z.infer; +``` + +- [ ] **Step 2: model.schema.spec.ts'i güncelle** + +```ts +import { describe, it, expect } from "vitest"; +import { ModelNodeSchema } from "./model.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-22T10:30:00.000Z", + updatedAt: "2026-05-22T10:30:00.000Z", +}; + +const validProperties = { + ClassName: "User", + Description: "Kullanıcı entity", + TableRef: "users", + Properties: [ + { Name: "id", Type: "string", IsNullable: false, IsCollection: false }, + { Name: "orders", Type: "Order", IsNullable: false, IsCollection: true, RelationType: "OneToMany", RelatedModelRef: "Order" }, + ], + Methods: [ + { MethodName: "fullName", Visibility: "public", Parameters: [], ReturnType: "string", IsAsync: false, IsStatic: false }, + ], +}; + +describe("ModelNodeSchema (enriched)", () => { + it("geçerli Model + relation parse eder", () => { + const n = ModelNodeSchema.parse({ ...validBase, type: "Model", properties: validProperties }); + expect(n.properties.Properties[1].RelationType).toBe("OneToMany"); + expect(n.properties.TableRef).toBe("users"); + }); + + it("method signature (visibility/params/async) parse eder", () => { + const n = ModelNodeSchema.parse({ + ...validBase, type: "Model", + properties: { ...validProperties, Methods: [{ MethodName: "save", Visibility: "private", Parameters: [{ Name: "tx", Type: "Transaction", Optional: true }], ReturnType: "Promise", IsAsync: true, IsStatic: false }] }, + }); + expect(n.properties.Methods[0].IsAsync).toBe(true); + expect(n.properties.Methods[0].Parameters[0].Optional).toBe(true); + }); + + it("Properties default'lar (IsNullable/IsCollection false)", () => { + const n = ModelNodeSchema.parse({ + ...validBase, type: "Model", + properties: { ClassName: "X", Description: "d", Properties: [{ Name: "a", Type: "string" }] }, + }); + expect(n.properties.Properties[0].IsNullable).toBe(false); + expect(n.properties.Methods).toEqual([]); + }); + + it("geçersiz RelationType reddeder", () => { + expect(() => ModelNodeSchema.parse({ + ...validBase, type: "Model", + properties: { ...validProperties, Properties: [{ Name: "a", Type: "X", RelationType: "HasMany" }] }, + })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test + commit** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/nodes/schemas/model.schema.spec.ts` +Expected: PASS (4 tests) + +```bash +git add src/nodes/schemas/model.schema.ts src/nodes/schemas/model.schema.spec.ts +git commit -m "feat(schemas): Model enrichment — relations + typed method signatures" +``` + +--- + +### Task 5: Enum schema enrichment (key-value) + +**Files:** +- Modify: `src/nodes/schemas/enum.schema.ts` +- Modify: `src/nodes/schemas/enum.schema.spec.ts` + +- [ ] **Step 1: enum.schema.ts'i enrich et** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +export const EnumNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Enum"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + BackingType: z.enum(["string", "int"]).default("string"), + Values: z.array(z.object({ + Key: z.string().min(1), + Value: z.string().optional().describe("backing değer (yoksa Key)"), + Description: z.string().optional(), + })).min(1), + }).strict(), +}).strict(); + +export type EnumNode = z.infer; +``` + +- [ ] **Step 2: enum.schema.spec.ts'i güncelle** + +```ts +import { describe, it, expect } from "vitest"; +import { EnumNodeSchema } from "./enum.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-22T10:30:00.000Z", + updatedAt: "2026-05-22T10:30:00.000Z", +}; + +const validProperties = { + Name: "OrderStatus", + Description: "Sipariş durumu", + BackingType: "string" as const, + Values: [ + { Key: "PENDING", Value: "pending", Description: "Beklemede" }, + { Key: "SHIPPED", Value: "shipped" }, + { Key: "DELIVERED" }, + ], +}; + +describe("EnumNodeSchema (enriched key-value)", () => { + it("key-value + description parse eder", () => { + const n = EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: validProperties }); + expect(n.properties.Values).toHaveLength(3); + expect(n.properties.Values[0].Value).toBe("pending"); + }); + + it("BackingType default string", () => { + const { BackingType, ...rest } = validProperties; + const n = EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: rest }); + expect(n.properties.BackingType).toBe("string"); + }); + + it("int backing type kabul eder", () => { + const n = EnumNodeSchema.parse({ + ...validBase, type: "Enum", + properties: { ...validProperties, BackingType: "int", Values: [{ Key: "LOW", Value: "0" }, { Key: "HIGH", Value: "1" }] }, + }); + expect(n.properties.BackingType).toBe("int"); + }); + + it("Values boşsa reddeder", () => { + expect(() => EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: { ...validProperties, Values: [] } })).toThrow(); + }); + + it("Key boş string reddeder", () => { + expect(() => EnumNodeSchema.parse({ ...validBase, type: "Enum", properties: { ...validProperties, Values: [{ Key: "" }] } })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test + commit** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/nodes/schemas/enum.schema.spec.ts` +Expected: PASS (5 tests) + +```bash +git add src/nodes/schemas/enum.schema.ts src/nodes/schemas/enum.schema.spec.ts +git commit -m "feat(schemas): Enum enrichment — key-value + backing type" +``` + +--- + +### Task 6: View schema enrichment + +**Files:** +- Modify: `src/nodes/schemas/view.schema.ts` +- Modify: `src/nodes/schemas/view.schema.spec.ts` + +- [ ] **Step 1: view.schema.ts'i enrich et** + +```ts +import { z } from "zod"; +import { BaseNodeSchema } from "./base.schema"; + +export const ViewNodeSchema = BaseNodeSchema.extend({ + type: z.literal("View"), + properties: z.object({ + ViewName: z.string().min(1), + Description: z.string().min(1), + Definition: z.string().min(1).describe("SQL/aggregate tanımı"), + SourceTables: z.array(z.string().min(1)).min(1).describe("→ Table Name'leri"), + Materialized: z.boolean(), + Columns: z.array(z.object({ Name: z.string().min(1), DataType: z.string().min(1) })).default([]), + RefreshStrategy: z.enum(["onDemand", "scheduled", "onChange"]).optional().describe("materialized için"), + }).strict(), +}).strict(); + +export type ViewNode = z.infer; +``` + +- [ ] **Step 2: view.schema.spec.ts'i güncelle** + +```ts +import { describe, it, expect } from "vitest"; +import { ViewNodeSchema } from "./view.schema"; + +const validBase = { + id: "550e8400-e29b-41d4-a716-446655440000", + projectId: "550e8400-e29b-41d4-a716-446655440001", + position: { x: 0, y: 0 }, + createdAt: "2026-05-22T10:30:00.000Z", + updatedAt: "2026-05-22T10:30:00.000Z", +}; + +const validProperties = { + ViewName: "active_users", + Description: "Aktif kullanıcılar", + Definition: "SELECT id, email FROM users WHERE active = true", + SourceTables: ["users"], + Materialized: true, + Columns: [{ Name: "id", DataType: "UUID" }, { Name: "email", DataType: "VARCHAR" }], + RefreshStrategy: "scheduled" as const, +}; + +describe("ViewNodeSchema (enriched)", () => { + it("columns + refresh strategy parse eder", () => { + const n = ViewNodeSchema.parse({ ...validBase, type: "View", properties: validProperties }); + expect(n.properties.Columns).toHaveLength(2); + expect(n.properties.RefreshStrategy).toBe("scheduled"); + }); + + it("Columns default boş array", () => { + const { Columns, RefreshStrategy, ...rest } = validProperties; + const n = ViewNodeSchema.parse({ ...validBase, type: "View", properties: rest }); + expect(n.properties.Columns).toEqual([]); + }); + + it("geçersiz RefreshStrategy reddeder", () => { + expect(() => ViewNodeSchema.parse({ ...validBase, type: "View", properties: { ...validProperties, RefreshStrategy: "hourly" } })).toThrow(); + }); + + it("SourceTables boşsa reddeder", () => { + expect(() => ViewNodeSchema.parse({ ...validBase, type: "View", properties: { ...validProperties, SourceTables: [] } })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test + commit** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/nodes/schemas/view.schema.spec.ts` +Expected: PASS (4 tests) + +```bash +git add src/nodes/schemas/view.schema.ts src/nodes/schemas/view.schema.spec.ts +git commit -m "feat(schemas): View enrichment — columns + refresh strategy" +``` + +--- + +### Task 7: Veri ailesi fieldHints (UI metadata) + +**Files:** +- Modify: `src/node-types/registry.ts` +- Modify: `src/node-types/node-types.service.ts` +- Modify: `src/node-types/node-types.service.spec.ts` + +- [ ] **Step 1: registry.ts'e fieldHints ekle** + +`NodeTypeMetadata` interface'ine `fieldHints?: Record` ekle ve `make()` imzasını genişlet. Veri ailesi girişlerine hint ekle. Örnek (Table): + +```ts +// registry.ts — NodeTypeMetadata'ya alan ekle: +export interface NodeTypeMetadata { + id: NodeKind; + family: NodeFamily; + familyLabel: string; + description: string; + nameKey: string; + schema: z.ZodTypeAny; + fieldHints?: Record; +} +``` + +Table girişine `make(...)` sonrası elle `fieldHints` ata (make signature'ı bozmamak için registry objesinde post-assign veya make'e 7. param). En basit: NODE_TYPE_REGISTRY tanımından sonra: + +```ts +NODE_TYPE_REGISTRY.Table.fieldHints = { + "Columns.IsPrimaryKey": { badge: "PK", group: "constraints" }, + "Columns.IsNotNull": { badge: "NN", group: "constraints" }, + "Columns.IsUnique": { badge: "UQ", group: "constraints" }, + "Columns.AutoIncrement": { badge: "AI", group: "constraints" }, + "ForeignKeys": { badge: "FK", group: "constraints" }, + "Indexes": { badge: "IDX", group: "performance" }, +}; +NODE_TYPE_REGISTRY.DTO.fieldHints = { "Fields.ValidationRules": { badge: "VALID" } }; +NODE_TYPE_REGISTRY.Enum.fieldHints = { "Values": { badge: "ENUM" } }; +``` + +- [ ] **Step 2: node-types.service.ts getById'ye fieldHints ekle** + +`NodeTypeDetail`'e `fieldHints?` alanı ekle ve `getById`'de döndür: + +```ts +// node-types.service.ts getById içinde: +return { + ...this.toSummary(meta), + schema: zodV3ToOpenAPI(meta.schema as any), + fieldHints: meta.fieldHints ?? {}, +}; +``` +`NodeTypeDetail` interface'ine `fieldHints: Record` ekle. + +- [ ] **Step 3: node-types.service.spec.ts'e test ekle** + +```ts +it("getById Table fieldHints döner (PK/FK badge)", () => { + const d = service.getById("Table") as any; + expect(d.fieldHints["Columns.IsPrimaryKey"].badge).toBe("PK"); + expect(d.fieldHints["ForeignKeys"].badge).toBe("FK"); +}); +``` + +- [ ] **Step 4: Test + build + commit** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test src/node-types/ && pnpm build` +Expected: PASS + +```bash +git add src/node-types/ +git commit -m "feat(node-types): Veri ailesi fieldHints — UI badge metadata (PK/FK/NN/...)" +``` + +--- + +### Task 8: AI system prompt — Veri ailesi şema rehberi güncel + +**Files:** +- Modify: `src/ai/prompts/system-prompt.ts` + +- [ ] **Step 1: Veri ailesi şema satırlarını güncelle** + +`system-prompt.ts` içindeki `## NODE PROPERTIES ŞEMALARI` bölümünde Table/DTO/Model/Enum/View satırlarını zengin şemaya göre değiştir. Table örneği: + +``` +- **Table:** { TableName, Description, Columns: [{ Name, DataType, Length?, Precision?, Scale?, IsPrimaryKey, IsNotNull, IsUnique, AutoIncrement, DefaultValue?, EnumRef?, ... }], PrimaryKey?: {Columns[]}, ForeignKeys: [{ Columns[], ReferencesTable, ReferencesColumns[], OnDelete, OnUpdate }], UniqueConstraints: [], CheckConstraints: [], Indexes: [{ IndexName, Columns[], Type, IsUnique }] } + - DataType enum: INT/BIGINT/VARCHAR/TEXT/BOOLEAN/DATETIME/DATE/UUID/FLOAT/DECIMAL/JSON/ENUM + - Tek-kolon FK dahil TÜM foreign key'ler ForeignKeys[]'te (Column'da IsForeignKey YOK). +- **DTO:** { Name, Description, Fields: [{ Name, DataType, IsRequired, IsArray, ValidationRules: [{Rule,Value?}], NestedDTORef?, EnumRef? }] } +- **Model:** { ClassName, Description, TableRef?, Properties: [{ Name, Type, IsNullable, IsCollection, RelationType?, RelatedModelRef? }], Methods: [{ MethodName, Visibility, Parameters: [{Name,Type,Optional}], ReturnType, IsAsync }] } +- **Enum:** { Name, Description, BackingType, Values: [{ Key, Value?, Description? }] } +- **View:** { ViewName, Description, Definition, SourceTables[], Materialized, Columns: [{Name,DataType}], RefreshStrategy? } +``` + +- [ ] **Step 2: Build + commit** + +Run: `pnpm build` + +```bash +git add src/ai/prompts/system-prompt.ts +git commit -m "feat(ai): system prompt — Veri ailesi zengin şema rehberi" +``` + +--- + +### Task 9: Migration — Faz A node dönüşümü + +**Files:** +- Create: `src/neo4j/migrations/data/001-enrich-faz-a.ts` +- Modify: `package.json` (scripts) + +- [ ] **Step 1: Migration script yaz** + +```ts +// src/neo4j/migrations/data/001-enrich-faz-a.ts +import { Neo4jService } from "../../neo4j.service"; +import { env } from "../../../config/env"; + +/** Faz A: mevcut Veri ailesi node'larını zengin şemaya dönüştür. + * Idempotent — eksik zorunlu alanları default ile doldurur. */ +async function main() { + const svc = new Neo4jService({ uri: env.NEO4J_URI, user: env.NEO4J_USER, password: env.NEO4J_PASSWORD }); + await svc.onModuleInit(); + + const kinds = ["Table", "DTO", "Model", "Enum", "View"]; + let migrated = 0; + for (const kind of kinds) { + const res = await svc.run(`MATCH (n:\`${kind}\`) RETURN n.id AS id, n.properties AS props`); + for (const rec of res.records) { + const id = rec.get("id"); + const props = JSON.parse(rec.get("props")); + const next = enrich(kind, props); + await svc.run(`MATCH (n {id: $id}) SET n.properties = $props`, { id, props: JSON.stringify(next) }); + migrated++; + } + } + await svc.onModuleDestroy(); + console.log(`✓ Faz A migration: ${migrated} node dönüştürüldü.`); +} + +function enrich(kind: string, p: any): any { + if (kind === "Table") { + return { + ...p, + ForeignKeys: p.ForeignKeys ?? [], + UniqueConstraints: p.UniqueConstraints ?? [], + CheckConstraints: p.CheckConstraints ?? [], + Indexes: (p.Indexes ?? []).map((i: any) => ({ Type: "BTree", IsUnique: false, ...i })), + Columns: (p.Columns ?? []).map((c: any) => { + const { IsForeignKey, References, ...rest } = c; // eski alanları at + return rest; + }), + }; + } + if (kind === "DTO") { + return { ...p, Fields: (p.Fields ?? []).map((f: any) => ({ ValidationRules: [], ...f })) }; + } + if (kind === "Model") { + return { + ...p, + Properties: (p.Properties ?? []).map((pr: any) => ({ IsNullable: false, IsCollection: false, ...pr })), + Methods: (p.Methods ?? []).map((m: any) => ({ Visibility: "public", Parameters: [], IsAsync: false, IsStatic: false, ...m })), + }; + } + if (kind === "Enum") { + return { + ...p, + BackingType: p.BackingType ?? "string", + Values: (p.Values ?? []).map((v: any) => (typeof v === "string" ? { Key: v } : v)), + }; + } + if (kind === "View") { + return { ...p, Columns: p.Columns ?? [] }; + } + return p; +} + +main().catch((e) => { console.error("✗ Migration failed:", e); process.exit(1); }); +``` + +- [ ] **Step 2: package.json'a script ekle** + +`scripts`'e ekle: +```json +"migrate:data:faz-a": "tsx --env-file=.env src/neo4j/migrations/data/001-enrich-faz-a.ts" +``` + +- [ ] **Step 3: Migration'ı çalıştır (Neo4j açık)** + +Run: `pnpm neo4j:up && sleep 12 && pnpm migrate:data:faz-a` +Expected: `✓ Faz A migration: N node dönüştürüldü.` + +- [ ] **Step 4: Commit** + +```bash +git add src/neo4j/migrations/data/001-enrich-faz-a.ts package.json +git commit -m "feat(migration): Faz A node dönüşümü — Veri ailesi zengin şema" +``` + +--- + +### Task 10: E2E fixtures + tam test paketi + canlı doğrulama + +**Files:** +- Modify: `test/nodes.e2e-spec.ts` (Table/DTO/Model/Enum/View fixtures) + +- [ ] **Step 1: e2e Table/DTO/Enum/View fixtures'ı zengin şemaya güncelle** + +`test/nodes.e2e-spec.ts` `fixtures` objesinde Table'ı zengin şemaya uydur (ForeignKeys/Indexes default'lu, Column'dan IsForeignKey/References çıkar), DTO Fields'a ValidationRules ekle, Enum Values'u key-value yap, View'a Columns ekle. (Service/Controller fixtures Faz A'da değişmez.) + +- [ ] **Step 2: Tüm unit + e2e testleri çalıştır** + +Run: `NEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=x pnpm test` +Expected: tüm unit testler PASS + +Run: `pnpm test:e2e` +Expected: 9 e2e PASS + +- [ ] **Step 3: Server başlat + canlı AI doğrulama (DeepSeek veya Kimi)** + +```bash +set -a; source .env; set +a +LLM_GENERATION_PROVIDER=deepseek PORT=4000 node dist/main.js & +# proje oluştur + /ai/chat "users tablosu çiz: id (UUID PK), email (VARCHAR unique), status (ENUM UserStatus ref)" +``` +Expected: AI ≤3 denemede zengin Table üretir (EnumRef + constraints), `applied` dolu. + +- [ ] **Step 4: Build + commit + push** + +```bash +pnpm build +git add test/nodes.e2e-spec.ts +git commit -m "test(faz-a): e2e fixtures zengin Veri ailesi şemasına güncellendi" +git push origin main +``` + +--- + +## FAZ B — İş Mantığı + Erişim (yapısal görevler) + +Faz A pattern'iyle (schema enrich → spec güncelle → fieldHints → AI prompt → migration → test) her node için ayrı task. Spec Bölüm 4 tam alanları tanımlar. + +- [ ] **Task B1: Service enrichment** — Methods[] (Visibility, Parameters typed, ReturnDtoRef, Throws[]), Dependencies[] (DI). Spec §4 Service. +- [ ] **Task B2: Orchestrator enrichment** — Steps[] (StepName, ServiceRef, CompensationAction, OnFailure). Spec §4. +- [ ] **Task B3: Worker enrichment** — RetryPolicy obj (MaxRetries, BackoffStrategy), Concurrency, IsEnabled. +- [ ] **Task B4: EventHandler enrichment** — QueueRef, RetryPolicy, DeadLetterQueue. +- [ ] **Task B5: Controller enrichment** — Endpoints[] +PathParams/QueryParams/StatusCodes/MiddlewareRefs/RateLimit. +- [ ] **Task B6: MessageQueue enrichment** — DeliveryGuarantee, DeadLetterQueue, RetentionSeconds. +- [ ] **Task B7: APIGateway enrichment** — Routes[] (Path, TargetRef, Methods, AuthRequired), AuthMode, CorsEnabled. +- [ ] **Task B8: Faz B altyapı** — GRAPH_SCHEMA_VERSION bump (3), fieldHints, system-prompt güncel, migration `002-enrich-faz-b.ts`, test + canlı AI doğrulama, commit+push. + +--- + +## FAZ C — Altyapı + İstemci + Güvenlik + Konfig + Yapı (yapısal görevler) + +Spec Bölüm 5 tam alanları tanımlar. Faz A pattern'iyle. + +- [ ] **Task C1: Repository** — EntityRef, CustomQueries[] (QueryName, QueryType, Parameters, ReturnType), BaseClass, IsCached. +- [ ] **Task C2: Cache** — EvictionPolicy, MaxSizeMB, Serialization. +- [ ] **Task C3: ExternalService** — Endpoints[], RetryPolicy, RateLimit, CircuitBreaker. +- [ ] **Task C4: FrontendApp** — StateManagement, StylingApproach, Routes[]. +- [ ] **Task C5: UIComponent** — Props/State typed, Events[], ChildComponentRefs[]. +- [ ] **Task C6: Middleware** — MiddlewareType, Config. +- [ ] **Task C7: EnvironmentVariable** — DefaultValue, IsRequired, ValidationPattern. +- [ ] **Task C8: Exception** — ErrorCode, ParentExceptionRef. +- [ ] **Task C9: Module** — ExposedServices[], Dependencies[]. +- [ ] **Task C10: Faz C altyapı** — GRAPH_SCHEMA_VERSION bump (4), fieldHints, system-prompt, migration `003-enrich-faz-c.ts`, test + canlı AI doğrulama, commit+push. + +--- + +## Faz A Çıkış Kriterleri (Spec §7 ile) + +- [x] Table/DTO/Model/Enum/View zengin (required + .describe()) — Task 2-6 +- [x] GRAPH_SCHEMA_VERSION=2 + migration — Task 1, 9 +- [x] create-node.dto.ts güncel (shape'ten otomatik türer — ek iş yok) +- [x] node-types fieldHints — Task 7 +- [x] system-prompt güncel — Task 8 +- [x] unit + service + e2e yeşil — Task 10 +- [x] AI canlı doğrulama (zengin Table) — Task 10 + +## Notlar +- `create-node.dto.ts` properties'i `XxxNodeSchema.shape.properties`'ten türetir → schema enrich edilince otomatik güncel, ek değişiklik yok. +- `NodesRepository.findNameKey` değişmez (TableName/Name/ClassName/ViewName aynı). +- Migration idempotent (eksik alan varsa doldurur, varsa korur). Mevcut DB az veri. +- Faz B/C kendi turlarında Faz A pattern'iyle detaylanır; her biri ayrı plan veya bu planın devamı. diff --git a/apps/server/docs/plans/2026-05-22-phase4-graphrag-implementation.md b/apps/server/docs/plans/2026-05-22-phase4-graphrag-implementation.md new file mode 100644 index 0000000..2865a73 --- /dev/null +++ b/apps/server/docs/plans/2026-05-22-phase4-graphrag-implementation.md @@ -0,0 +1,1274 @@ +# Phase 4 — Pattern Library + GraphRAG Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Küratörlü mimari pattern kütüphanesi + Neo4j native vector index + Bedrock embeddings ile AI agent'a retrieval-augmented üretim eklemek. + +**Architecture:** `:Pattern` node'ları (alt-graf + açıklama + embedding) Neo4j'de izole tutulur; native vector index ile semantik aranır. `EmbeddingsService` (provider-abstracted, bedrock-mantle default) sorguyu vektörler; `AiService.chat` LLM turundan önce top-K pattern'i system prompt'a otomatik enjekte eder. Doluluk: seed + promote. + +**Tech Stack:** NestJS 11, Neo4j 5-community (native vector index 5.13+), Zod + nestjs-zod, `@langchain/openai` `OpenAIEmbeddings`, Vitest + Testcontainers. + +**Referans desenler (oku):** `src/ai/providers/llm.factory.ts` (provider abstraction), `src/neo4j/migrations/data/001-enrich-faz-a.ts` (TS migration), `src/nodes/nodes.repository.ts` (Cypher repo + JSON.stringify properties), `src/graph/dto/apply-graph.dto.ts` (graphJson formatı), `src/common/envelope.ts` (`ok()`), `src/node-types/node-types.controller.ts` (Scalar `@ApiOperation` decorator deseni). + +--- + +## File Structure + +- `src/config/env.ts` — EMBED_* env değişkenleri (modify) +- `src/embeddings/embeddings.types.ts` — `IEmbeddings` interface + `EMBEDDINGS` DI token +- `src/embeddings/embeddings.factory.ts` — `OpenAIEmbeddings` üretimi (bedrock-mantle) +- `src/embeddings/embeddings.service.ts` — `IEmbeddings` impl (embed/embedBatch/isConfigured) +- `src/embeddings/embeddings.module.ts` — provider export +- `src/patterns/schemas/pattern.schema.ts` — Zod: PatternGraph, Pattern, CreatePattern, SearchPattern +- `src/patterns/dto/*.dto.ts` — createZodDto sınıfları +- `src/patterns/patterns.repository.ts` — Cypher CRUD + vector search +- `src/patterns/patterns.service.ts` — embed+store, search, promote +- `src/patterns/patterns.controller.ts` — endpoint'ler +- `src/patterns/patterns.module.ts` — modül +- `src/patterns/seed/canonical-patterns.ts` — ~12 kanonik pattern verisi +- `src/patterns/seed/seed.ts` — seed runner (`pnpm seed:patterns`) +- `src/neo4j/migrations/data/004-pattern-vector-index.ts` — vektör index (env EMBED_DIM) +- `src/ai/prompts/system-prompt.ts` — `buildSystemPrompt(graph, patterns?)` (modify) +- `src/ai/ai.service.ts` — retrieval + enjeksiyon (modify) +- `src/ai/ai.module.ts` — EmbeddingsModule + PatternsModule import (modify) +- `src/app.module.ts` — PatternsModule + EmbeddingsModule (modify) +- `test/patterns.e2e-spec.ts` — fake embedder + vector index round-trip +- `package.json` — scripts (modify) + +--- + +### Task 1: bedrock-mantle `/embeddings` doğrulama (spike) + +**Amaç:** Embedding endpoint'i, model id'si ve **boyutu** (vector index'i belirler) doğrulamak. TDD değil — keşif; sonucu env default'larına yazarız. + +**Files:** geçici script (commit edilmez). + +- [ ] **Step 1: Geçici doğrulama scripti yaz ve çalıştır** + +Repo kökünde `verify-embed.ts` (sonra silinecek): + +```ts +import { OpenAIEmbeddings } from "@langchain/openai"; +import { env } from "./src/config/env"; + +const CANDIDATES = ["amazon.titan-embed-text-v2:0", "cohere.embed-english-v3", "amazon.titan-embed-text-v1"]; + +(async () => { + for (const model of CANDIDATES) { + try { + const e = new OpenAIEmbeddings({ + model, + apiKey: env.BEDROCK_API_KEY, + configuration: { baseURL: env.BEDROCK_BASE_URL }, + }); + const vec = await e.embedQuery("merhaba mimari test"); + console.log(`✓ ${model} → dim=${vec.length}`); + } catch (err: any) { + console.log(`✗ ${model} → ${err?.status ?? ""} ${err?.message?.slice(0, 120)}`); + } + } +})(); +``` + +Run: `pnpm exec tsx --env-file=.env verify-embed.ts` + +- [ ] **Step 2: Sonucu kaydet, scripti sil** + +Çalışan ilk modelin id'si → `EMBED_MODEL` default'u; `dim` → `EMBED_DIM` default'u (Task 2). +**Eğer hiçbiri çalışmazsa** (mantle embeddings sunmuyorsa): `EMBED_PROVIDER=local` yolunu seç — +`@langchain/community` + `@xenova/transformers` ile `Xenova/all-MiniLM-L6-v2` (dim=384). Bu durumda +Task 3 factory'sine local branch eklenir (plan Task 3 Step 3'te belirtildi). + +Run: `rm -f verify-embed.ts` +Expected: temizlendi. Karar bir cümleyle commit mesajına / Task 2'ye yazılır. + +--- + +### Task 2: Embedding env değişkenleri + +**Files:** +- Modify: `src/config/env.ts` +- Test: `src/config/env.spec.ts` + +- [ ] **Step 1: Failing test ekle** + +`src/config/env.spec.ts` içine: + +```ts +it("embedding default'larını doldurur", () => { + const e = parseEnv({ NEO4J_URI: "bolt://localhost:7687", NEO4J_USER: "neo4j", NEO4J_PASSWORD: "x" }); + expect(e.EMBED_PROVIDER).toBe("bedrock"); + expect(e.EMBED_DIM).toBeGreaterThan(0); +}); +``` + +- [ ] **Step 2: Test fail (EMBED_PROVIDER undefined)** + +Run: `pnpm vitest run src/config/env.spec.ts` +Expected: FAIL. + +- [ ] **Step 3: env.ts'e alanları ekle** + +`EnvSchema` içine (DEEPSEEK_MODEL satırından sonra), Task 1'de bulunan değerlerle: + +```ts + // ── Embeddings (Phase 4 GraphRAG) ── + EMBED_PROVIDER: z.enum(["bedrock", "local"]).default("bedrock"), + EMBED_MODEL: z.string().default("amazon.titan-embed-text-v2:0"), // Task 1 sonucu + EMBED_DIM: z.coerce.number().int().positive().default(1024), // Task 1 sonucu + EMBED_MIN_SCORE: z.coerce.number().min(0).max(1).default(0.7), + EMBED_TOP_K: z.coerce.number().int().positive().default(3), +``` + +(bedrock embeddings, mevcut `BEDROCK_API_KEY` + `BEDROCK_BASE_URL`'i yeniden kullanır — yeni anahtar yok.) + +- [ ] **Step 4: Test pass** + +Run: `pnpm vitest run src/config/env.spec.ts` +Expected: PASS (5 test). + +- [ ] **Step 5: Commit** + +```bash +git add src/config/env.ts src/config/env.spec.ts +git commit -m "feat(config): embedding env (EMBED_PROVIDER/MODEL/DIM/TOP_K/MIN_SCORE)" +``` + +--- + +### Task 3: Embeddings provider abstraction + +**Files:** +- Create: `src/embeddings/embeddings.types.ts`, `embeddings.factory.ts`, `embeddings.service.ts`, `embeddings.module.ts` +- Test: `src/embeddings/embeddings.service.spec.ts` + +- [ ] **Step 1: Interface + token (types)** + +`src/embeddings/embeddings.types.ts`: + +```ts +export const EMBEDDINGS = Symbol("EMBEDDINGS"); + +export interface IEmbeddings { + isConfigured(): boolean; + embed(text: string): Promise; + embedBatch(texts: string[]): Promise; +} +``` + +- [ ] **Step 2: Factory (llm.factory ikizi)** + +`src/embeddings/embeddings.factory.ts`: + +```ts +import { OpenAIEmbeddings } from "@langchain/openai"; +import { env } from "../config/env"; + +/** bedrock-mantle = OpenAI-uyumlu /embeddings; mevcut BEDROCK_API_KEY ile. */ +export function makeBedrockEmbedder(): OpenAIEmbeddings { + if (!env.BEDROCK_API_KEY || !env.BEDROCK_BASE_URL) { + throw new Error("BEDROCK_API_KEY ve BEDROCK_BASE_URL gerekli (embeddings=bedrock)."); + } + return new OpenAIEmbeddings({ + model: env.EMBED_MODEL, + apiKey: env.BEDROCK_API_KEY, + configuration: { baseURL: env.BEDROCK_BASE_URL }, + }); +} + +export function embeddingsConfigured(): boolean { + if (env.EMBED_PROVIDER === "local") return true; + return !!(env.BEDROCK_API_KEY && env.BEDROCK_BASE_URL); +} +``` + +(Task 1 local'e karar verdiyse: `makeLocalEmbedder()` ekle — `@langchain/community` +`HuggingFaceTransformersEmbeddings` `Xenova/all-MiniLM-L6-v2` — ve `getEmbedderImpl()` +provider'a göre seçsin.) + +- [ ] **Step 3: Service (IEmbeddings impl)** + +`src/embeddings/embeddings.service.ts`: + +```ts +import { Injectable, Logger } from "@nestjs/common"; +import type { Embeddings } from "@langchain/core/embeddings"; +import { makeBedrockEmbedder, embeddingsConfigured } from "./embeddings.factory"; +import type { IEmbeddings } from "./embeddings.types"; + +@Injectable() +export class EmbeddingsService implements IEmbeddings { + private readonly logger = new Logger(EmbeddingsService.name); + private embedder: Embeddings | null = null; + + isConfigured(): boolean { + return embeddingsConfigured(); + } + + private get client(): Embeddings { + if (!this.embedder) this.embedder = makeBedrockEmbedder(); + return this.embedder; + } + + async embed(text: string): Promise { + return this.client.embedQuery(text); + } + + async embedBatch(texts: string[]): Promise { + return this.client.embedDocuments(texts); + } +} +``` + +- [ ] **Step 4: Module** + +`src/embeddings/embeddings.module.ts`: + +```ts +import { Module } from "@nestjs/common"; +import { EmbeddingsService } from "./embeddings.service"; +import { EMBEDDINGS } from "./embeddings.types"; + +@Module({ + providers: [EmbeddingsService, { provide: EMBEDDINGS, useExisting: EmbeddingsService }], + exports: [EMBEDDINGS, EmbeddingsService], +}) +export class EmbeddingsModule {} +``` + +- [ ] **Step 5: Failing test** + +`src/embeddings/embeddings.service.spec.ts`: + +```ts +import { describe, it, expect, vi } from "vitest"; +import { EmbeddingsService } from "./embeddings.service"; + +describe("EmbeddingsService", () => { + it("isConfigured boolean döner", () => { + expect(typeof new EmbeddingsService().isConfigured()).toBe("boolean"); + }); + + it("embed, embedder.embedQuery'yi çağırır", async () => { + const svc = new EmbeddingsService(); + (svc as any).embedder = { embedQuery: vi.fn().mockResolvedValue([0.1, 0.2]) }; + expect(await svc.embed("x")).toEqual([0.1, 0.2]); + }); +}); +``` + +- [ ] **Step 6: Test pass + commit** + +Run: `pnpm vitest run src/embeddings` +Expected: PASS (2 test). + +```bash +git add src/embeddings/ +git commit -m "feat(embeddings): provider-abstracted embeddings (bedrock-mantle default)" +``` + +--- + +### Task 4: Pattern Zod şeması + +**Files:** +- Create: `src/patterns/schemas/pattern.schema.ts` +- Test: `src/patterns/schemas/pattern.schema.spec.ts` + +- [ ] **Step 1: Şema (graphJson = apply mutations formatı)** + +`src/patterns/schemas/pattern.schema.ts`: + +```ts +import { z } from "zod"; +import { EdgeKindSchema } from "../../edges/schemas/edge.schema"; + +// graphJson: GraphService.apply girdi formatıyla AYNI (tempId tabanlı) → pattern +// doğrudan apply edilebilir. +export const PatternGraphSchema = z.object({ + nodes: z.array(z.object({ + tempId: z.string().min(1), + type: z.string().min(1), + properties: z.record(z.unknown()), + }).strict()).min(1), + edges: z.array(z.object({ + sourceTempId: z.string().min(1), + targetTempId: z.string().min(1), + edgeType: EdgeKindSchema, + label: z.string().optional(), + }).strict()).default([]), +}).strict(); + +export const CreatePatternSchema = z.object({ + name: z.string().min(1), + description: z.string().min(1), + tags: z.array(z.string().min(1)).default([]), + graph: PatternGraphSchema, +}).strict(); + +export const SearchPatternSchema = z.object({ + query: z.string().min(1), + k: z.number().int().positive().max(20).optional(), + minScore: z.number().min(0).max(1).optional(), +}).strict(); + +export type PatternGraph = z.infer; +export type CreatePatternInput = z.infer; +export type SearchPatternInput = z.infer; + +// Saklanan + dönen tam Pattern (embedding API yanıtında dönmez). +export interface StoredPattern { + id: string; + name: string; + description: string; + tags: string[]; + graph: PatternGraph; + source: "seed" | "promoted"; + createdAt: string; +} +export interface PatternSummary { + id: string; name: string; description: string; tags: string[]; + source: string; createdAt: string; nodeCount: number; edgeCount: number; +} +``` + +- [ ] **Step 2: Failing test** + +`src/patterns/schemas/pattern.schema.spec.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { CreatePatternSchema, PatternGraphSchema } from "./pattern.schema"; + +const graph = { + nodes: [{ tempId: "t_ctrl", type: "Controller", properties: { ControllerName: "X" } }], + edges: [], +}; + +describe("CreatePatternSchema", () => { + it("geçerli pattern'i parse eder, tags default boş", () => { + const p = CreatePatternSchema.parse({ name: "n", description: "d", graph }); + expect(p.tags).toEqual([]); + expect(p.graph.nodes).toHaveLength(1); + }); + it("graph.nodes boşsa fırlatır", () => { + expect(() => PatternGraphSchema.parse({ nodes: [], edges: [] })).toThrow(); + }); + it("geçersiz edgeType reddeder", () => { + expect(() => PatternGraphSchema.parse({ + nodes: graph.nodes, + edges: [{ sourceTempId: "a", targetTempId: "b", edgeType: "BOGUS" }], + })).toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test pass + commit** + +Run: `pnpm vitest run src/patterns/schemas` +Expected: PASS (3 test). + +```bash +git add src/patterns/schemas/ +git commit -m "feat(patterns): Pattern Zod şeması (graphJson = apply formatı)" +``` + +--- + +### Task 5: Vektör index migration + +**Files:** +- Create: `src/neo4j/migrations/data/004-pattern-vector-index.ts` +- Modify: `package.json` (scripts) + +- [ ] **Step 1: Migration scripti** + +`src/neo4j/migrations/data/004-pattern-vector-index.ts`: + +```ts +import { Neo4jService } from "../../neo4j.service"; +import { env } from "../../../config/env"; + +/** :Pattern(embedding) için native vektör index. Idempotent (IF NOT EXISTS). + * Boyut env.EMBED_DIM'den — model değişirse index drop + yeniden oluştur. */ +async function main(): Promise { + const svc = new Neo4jService({ uri: env.NEO4J_URI, user: env.NEO4J_USER, password: env.NEO4J_PASSWORD }); + await svc.onModuleInit(); + await svc.run( + `CREATE VECTOR INDEX pattern_embedding IF NOT EXISTS + FOR (p:Pattern) ON (p.embedding) + OPTIONS { indexConfig: { + \`vector.dimensions\`: $dim, + \`vector.similarity_function\`: 'cosine' + } }`, + { dim: env.EMBED_DIM }, + ); + await svc.onModuleDestroy(); + console.log(`✓ pattern_embedding vektör index hazır (dim=${env.EMBED_DIM}).`); +} + +main().catch((e) => { console.error("✗ Index migration failed:", e); process.exit(1); }); +``` + +- [ ] **Step 2: package.json script** + +`scripts`'e ekle (migrate:data:faz-c satırından sonra): + +```json +"migrate:patterns-index": "tsx --env-file=.env src/neo4j/migrations/data/004-pattern-vector-index.ts" +``` + +- [ ] **Step 3: Çalıştır (Neo4j açık)** + +Run: `pnpm migrate:patterns-index` +Expected: `✓ pattern_embedding vektör index hazır (dim=...).` + +Doğrula: `docker exec solarch-neo4j cypher-shell -u neo4j -p solarch_dev_password "SHOW INDEXES YIELD name WHERE name='pattern_embedding' RETURN name"` → `pattern_embedding`. + +- [ ] **Step 4: Commit** + +```bash +git add src/neo4j/migrations/data/004-pattern-vector-index.ts package.json +git commit -m "feat(neo4j): :Pattern vektör index migration (env EMBED_DIM, cosine)" +``` + +--- + +### Task 6: Patterns repository + +**Files:** +- Create: `src/patterns/patterns.repository.ts` +- Test: `src/patterns/patterns.repository.spec.ts` + +- [ ] **Step 1: Repository** + +`src/patterns/patterns.repository.ts`: + +```ts +import { Injectable } from "@nestjs/common"; +import { Neo4jService } from "../neo4j/neo4j.service"; +import type { StoredPattern, PatternSummary } from "./schemas/pattern.schema"; + +export interface PatternSearchHit { pattern: StoredPattern; score: number; } + +@Injectable() +export class PatternsRepository { + constructor(private readonly neo4j: Neo4jService) {} + + async create(p: StoredPattern, embedding: number[]): Promise { + await this.neo4j.run( + `CREATE (p:Pattern { + id: $id, name: $name, description: $description, tags: $tags, + graphJson: $graphJson, source: $source, + createdAt: datetime($createdAt), embedding: $embedding + })`, + { + id: p.id, name: p.name, description: p.description, tags: p.tags, + graphJson: JSON.stringify(p.graph), source: p.source, + createdAt: p.createdAt, embedding, + }, + ); + } + + async list(): Promise { + const res = await this.neo4j.run( + `MATCH (p:Pattern) RETURN p ORDER BY p.createdAt DESC`, + ); + return res.records.map((r) => toSummary(r.get("p").properties)); + } + + async getById(id: string): Promise { + const res = await this.neo4j.run(`MATCH (p:Pattern {id: $id}) RETURN p`, { id }); + if (res.records.length === 0) return null; + return toStored(res.records[0].get("p").properties); + } + + async delete(id: string): Promise { + const res = await this.neo4j.run( + `MATCH (p:Pattern {id: $id}) DELETE p RETURN 1 AS d`, { id }, + ); + return res.records.length > 0; + } + + async findByName(name: string): Promise { + const res = await this.neo4j.run(`MATCH (p:Pattern {name: $name}) RETURN p LIMIT 1`, { name }); + return res.records.length > 0; + } + + /** Native vektör arama: cosine top-K + minScore filtresi. */ + async search(embedding: number[], k: number, minScore: number): Promise { + const res = await this.neo4j.run( + `CALL db.index.vector.queryNodes('pattern_embedding', $k, $embedding) + YIELD node, score + WHERE score >= $minScore + RETURN node, score ORDER BY score DESC`, + { k, embedding, minScore }, + ); + return res.records.map((r) => ({ + pattern: toStored(r.get("node").properties), + score: r.get("score"), + })); + } +} + +function toStored(p: any): StoredPattern { + return { + id: p.id, name: p.name, description: p.description, + tags: p.tags ?? [], graph: JSON.parse(p.graphJson), + source: p.source, createdAt: new Date(p.createdAt).toISOString(), + }; +} +function toSummary(p: any): PatternSummary { + const g = JSON.parse(p.graphJson); + return { + id: p.id, name: p.name, description: p.description, tags: p.tags ?? [], + source: p.source, createdAt: new Date(p.createdAt).toISOString(), + nodeCount: g.nodes.length, edgeCount: g.edges.length, + }; +} +``` + +- [ ] **Step 2: Failing test (mock Neo4jService)** + +`src/patterns/patterns.repository.spec.ts`: + +```ts +import { describe, it, expect, vi } from "vitest"; +import { PatternsRepository } from "./patterns.repository"; + +const neo4j = { run: vi.fn() }; +const repo = new PatternsRepository(neo4j as any); + +describe("PatternsRepository", () => { + it("search, vektör index'i çağırır ve hit map'ler", async () => { + neo4j.run.mockResolvedValueOnce({ + records: [{ + get: (k: string) => k === "score" ? 0.91 + : { properties: { id: "1", name: "n", description: "d", tags: [], graphJson: '{"nodes":[],"edges":[]}', source: "seed", createdAt: "2026-05-22T00:00:00.000Z" } }, + }], + }); + const hits = await repo.search([0.1, 0.2], 3, 0.7); + expect(hits[0].score).toBe(0.91); + expect(neo4j.run.mock.calls[0][0]).toContain("db.index.vector.queryNodes"); + expect(neo4j.run.mock.calls[0][1]).toEqual({ k: 3, embedding: [0.1, 0.2], minScore: 0.7 }); + }); + + it("getById yoksa null döner", async () => { + neo4j.run.mockResolvedValueOnce({ records: [] }); + expect(await repo.getById("x")).toBeNull(); + }); +}); +``` + +- [ ] **Step 3: Test pass + commit** + +Run: `pnpm vitest run src/patterns/patterns.repository.spec.ts` +Expected: PASS (2 test). + +```bash +git add src/patterns/patterns.repository.ts src/patterns/patterns.repository.spec.ts +git commit -m "feat(patterns): repository — Cypher CRUD + native vektör arama" +``` + +--- + +### Task 7: Patterns service + +**Files:** +- Create: `src/patterns/patterns.service.ts` +- Test: `src/patterns/patterns.service.spec.ts` + +- [ ] **Step 1: Service** + +`src/patterns/patterns.service.ts`: + +```ts +import { Injectable, Inject, NotFoundException, ServiceUnavailableException } from "@nestjs/common"; +import { randomUUID } from "node:crypto"; +import { PatternsRepository, type PatternSearchHit } from "./patterns.repository"; +import { ProjectsRepository } from "../projects/projects.repository"; +import { EMBEDDINGS, type IEmbeddings } from "../embeddings/embeddings.types"; +import type { CreatePatternInput, StoredPattern, PatternSummary, PatternGraph } from "./schemas/pattern.schema"; + +@Injectable() +export class PatternsService { + constructor( + private readonly repo: PatternsRepository, + private readonly projectsRepo: ProjectsRepository, + @Inject(EMBEDDINGS) private readonly embeddings: IEmbeddings, + ) {} + + private embedText(p: { name: string; description: string; tags: string[] }): string { + return `${p.name}\n${p.description}\n${p.tags.join(" ")}`; + } + + async create(input: CreatePatternInput, source: "seed" | "promoted" = "seed"): Promise { + this.assertEmbeddings(); + const stored: StoredPattern = { + id: randomUUID(), name: input.name, description: input.description, + tags: input.tags, graph: input.graph, source, createdAt: new Date().toISOString(), + }; + const vec = await this.embeddings.embed(this.embedText(stored)); + await this.repo.create(stored, vec); + return summarize(stored); + } + + list(): Promise { return this.repo.list(); } + + async getById(id: string): Promise { + const p = await this.repo.getById(id); + if (!p) throw new NotFoundException({ code: "ERR_PATTERN_NOT_FOUND", message: `Pattern '${id}' bulunamadı.` }); + return p; + } + + async delete(id: string): Promise { + if (!(await this.repo.delete(id))) + throw new NotFoundException({ code: "ERR_PATTERN_NOT_FOUND", message: `Pattern '${id}' bulunamadı.` }); + } + + /** Sorgu metnini embed edip top-K döner. Embedding yoksa boş (degrade). */ + async search(query: string, k: number, minScore: number): Promise { + if (!this.embeddings.isConfigured()) return []; + const vec = await this.embeddings.embed(query); + return this.repo.search(vec, k, minScore); + } + + /** Proje grafiğinden pattern terfi. nodeIds verilmezse tüm proje. */ + async promoteFromProject( + projectId: string, + input: { name: string; description: string; tags?: string[]; nodeIds?: string[] }, + ): Promise { + const project = await this.projectsRepo.getById(projectId); + if (!project) throw new NotFoundException({ code: "ERR_PROJECT_NOT_FOUND", message: `Project '${projectId}' bulunamadı.` }); + const { nodes, edges } = await this.projectsRepo.getGraph(projectId); + + const selected = input.nodeIds?.length + ? nodes.filter((n) => input.nodeIds!.includes(n.id)) + : nodes; + if (selected.length === 0) + throw new NotFoundException({ code: "ERR_PATTERN_NODE_NOT_FOUND", message: "Seçili nodeIds projede bulunamadı." }); + + // gerçek id → tempId; sadece seçili node'lar arası edge'ler. + const idToTemp = new Map(); + selected.forEach((n, i) => idToTemp.set(n.id, `t_${i}_${n.type.toLowerCase()}`)); + const graph: PatternGraph = { + nodes: selected.map((n) => ({ tempId: idToTemp.get(n.id)!, type: n.type, properties: n.properties })), + edges: edges + .filter((e) => idToTemp.has(e.sourceNodeId) && idToTemp.has(e.targetNodeId)) + .map((e) => ({ + sourceTempId: idToTemp.get(e.sourceNodeId)!, + targetTempId: idToTemp.get(e.targetNodeId)!, + edgeType: e.kind, + label: e.properties?.Label, + })), + }; + return this.create({ name: input.name, description: input.description, tags: input.tags ?? [], graph }, "promoted"); + } + + private assertEmbeddings(): void { + if (!this.embeddings.isConfigured()) + throw new ServiceUnavailableException({ code: "ERR_EMBEDDINGS_NOT_CONFIGURED", message: "Embedding sağlayıcı yapılandırılmamış (BEDROCK_API_KEY)." }); + } +} + +function summarize(p: StoredPattern): PatternSummary { + return { + id: p.id, name: p.name, description: p.description, tags: p.tags, + source: p.source, createdAt: p.createdAt, + nodeCount: p.graph.nodes.length, edgeCount: p.graph.edges.length, + }; +} +``` + +- [ ] **Step 2: Failing test (mock repo + mock embeddings)** + +`src/patterns/patterns.service.spec.ts`: + +```ts +import { describe, it, expect, vi } from "vitest"; +import { PatternsService } from "./patterns.service"; + +const graph = { nodes: [{ tempId: "t", type: "Controller", properties: {} }], edges: [] }; + +function make(embConfigured = true) { + const repo = { create: vi.fn(), list: vi.fn(), getById: vi.fn(), delete: vi.fn(), search: vi.fn().mockResolvedValue([]) }; + const projectsRepo = { getById: vi.fn(), getGraph: vi.fn() }; + const embeddings = { isConfigured: () => embConfigured, embed: vi.fn().mockResolvedValue([0.1, 0.2]), embedBatch: vi.fn() }; + return { svc: new PatternsService(repo as any, projectsRepo as any, embeddings as any), repo, projectsRepo, embeddings }; +} + +describe("PatternsService", () => { + it("create embed edip repo.create çağırır", async () => { + const { svc, repo, embeddings } = make(); + await svc.create({ name: "n", description: "d", tags: [], graph } as any); + expect(embeddings.embed).toHaveBeenCalled(); + expect(repo.create).toHaveBeenCalledWith(expect.objectContaining({ name: "n", source: "seed" }), [0.1, 0.2]); + }); + + it("embedding yoksa search boş döner (degrade)", async () => { + const { svc, embeddings } = make(false); + expect(await svc.search("x", 3, 0.7)).toEqual([]); + expect(embeddings.embed).not.toHaveBeenCalled(); + }); + + it("embedding yoksa create 503 fırlatır", async () => { + const { svc } = make(false); + await expect(svc.create({ name: "n", description: "d", tags: [], graph } as any)).rejects.toThrow(); + }); + + it("promote: olmayan proje 404", async () => { + const { svc, projectsRepo } = make(); + projectsRepo.getById.mockResolvedValue(null); + await expect(svc.promoteFromProject("p", { name: "n", description: "d" })).rejects.toThrow(); + }); +}); +``` + +- [ ] **Step 3: Test pass + commit** + +Run: `pnpm vitest run src/patterns/patterns.service.spec.ts` +Expected: PASS (4 test). + +```bash +git add src/patterns/patterns.service.ts src/patterns/patterns.service.spec.ts +git commit -m "feat(patterns): service — embed+store, search (degrade), promote" +``` + +--- + +### Task 8: Patterns controller + DTO + modül + +**Files:** +- Create: `src/patterns/dto/create-pattern.dto.ts`, `dto/search-pattern.dto.ts`, `dto/promote-pattern.dto.ts`, `patterns.controller.ts`, `patterns.module.ts` +- Modify: `src/app.module.ts` +- Test: `src/patterns/patterns.controller.spec.ts` + +- [ ] **Step 1: DTO sınıfları** + +`src/patterns/dto/create-pattern.dto.ts`: + +```ts +import { createZodDto } from "nestjs-zod"; +import { CreatePatternSchema } from "../schemas/pattern.schema"; +export class CreatePatternDto extends createZodDto(CreatePatternSchema) {} +``` + +`src/patterns/dto/search-pattern.dto.ts`: + +```ts +import { createZodDto } from "nestjs-zod"; +import { SearchPatternSchema } from "../schemas/pattern.schema"; +export class SearchPatternDto extends createZodDto(SearchPatternSchema) {} +``` + +`src/patterns/dto/promote-pattern.dto.ts`: + +```ts +import { z } from "zod"; +import { createZodDto } from "nestjs-zod"; +export const PromotePatternSchema = z.object({ + name: z.string().min(1), + description: z.string().min(1), + tags: z.array(z.string().min(1)).default([]), + nodeIds: z.array(z.string().uuid()).optional(), +}).strict(); +export type PromotePatternInput = z.infer; +export class PromotePatternDto extends createZodDto(PromotePatternSchema) {} +``` + +- [ ] **Step 2: Controller** + +`src/patterns/patterns.controller.ts`: + +```ts +import { Body, Controller, Delete, Get, HttpCode, Param, Post } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from "@nestjs/swagger"; +import { PatternsService } from "./patterns.service"; +import { CreatePatternDto } from "./dto/create-pattern.dto"; +import { SearchPatternDto } from "./dto/search-pattern.dto"; +import { PromotePatternDto } from "./dto/promote-pattern.dto"; +import { ok } from "../common/envelope"; +import { env } from "../config/env"; + +@ApiTags("Patterns (GraphRAG)") +@Controller() +export class PatternsController { + constructor(private readonly service: PatternsService) {} + + @Post("patterns") + @ApiOperation({ summary: "Pattern oluştur", description: "Alt-graf + açıklama; embed edilip vektör index'e yazılır." }) + @ApiResponse({ status: 201, description: "Oluşturulan pattern özeti." }) + async create(@Body() body: CreatePatternDto) { + return ok(await this.service.create(body as any)); + } + + @Get("patterns") + @ApiOperation({ summary: "Pattern listesi" }) + async list() { return ok(await this.service.list()); } + + @Get("patterns/:id") + @ApiOperation({ summary: "Tek pattern (graphJson dahil)" }) + @ApiParam({ name: "id", description: "Pattern UUID" }) + @ApiResponse({ status: 404, description: "ERR_PATTERN_NOT_FOUND" }) + async getById(@Param("id") id: string) { return ok(await this.service.getById(id)); } + + @Delete("patterns/:id") + @HttpCode(204) + @ApiOperation({ summary: "Pattern sil" }) + async delete(@Param("id") id: string) { await this.service.delete(id); } + + @Post("patterns/search") + @HttpCode(200) + @ApiOperation({ summary: "Semantik pattern arama", description: "Sorguyu embed eder, native vektör index'te top-K cosine döner." }) + async search(@Body() body: SearchPatternDto) { + const { query, k, minScore } = body as any; + return ok(await this.service.search(query, k ?? env.EMBED_TOP_K, minScore ?? env.EMBED_MIN_SCORE)); + } + + @Post("projects/:projectId/patterns/promote") + @ApiOperation({ summary: "Proje grafiğinden pattern terfi", description: "nodeIds verilmezse tüm proje grafiği pattern'e dönüşür." }) + @ApiParam({ name: "projectId", description: "Proje UUID" }) + @ApiResponse({ status: 404, description: "ERR_PROJECT_NOT_FOUND / ERR_PATTERN_NODE_NOT_FOUND" }) + async promote(@Param("projectId") projectId: string, @Body() body: PromotePatternDto) { + return ok(await this.service.promoteFromProject(projectId, body as any)); + } +} +``` + +- [ ] **Step 3: Module** + +`src/patterns/patterns.module.ts`: + +```ts +import { Module } from "@nestjs/common"; +import { Neo4jModule } from "../neo4j/neo4j.module"; +import { ProjectsModule } from "../projects/projects.module"; +import { EmbeddingsModule } from "../embeddings/embeddings.module"; +import { PatternsController } from "./patterns.controller"; +import { PatternsService } from "./patterns.service"; +import { PatternsRepository } from "./patterns.repository"; + +@Module({ + imports: [Neo4jModule, ProjectsModule, EmbeddingsModule], + controllers: [PatternsController], + providers: [PatternsService, PatternsRepository], + exports: [PatternsService], +}) +export class PatternsModule {} +``` + +- [ ] **Step 4: app.module.ts'e ekle** + +`src/app.module.ts` `imports` dizisine `PatternsModule` ve `EmbeddingsModule` ekle (import satırlarıyla birlikte). Doğrula: `Neo4jModule` ve `ProjectsModule` zaten export ediyor (mevcut desen). + +- [ ] **Step 5: Controller smoke test** + +`src/patterns/patterns.controller.spec.ts`: + +```ts +import { describe, it, expect, vi } from "vitest"; +import { PatternsController } from "./patterns.controller"; + +describe("PatternsController", () => { + const service = { create: vi.fn().mockResolvedValue({ id: "1" }), search: vi.fn().mockResolvedValue([]), list: vi.fn().mockResolvedValue([]) }; + const c = new PatternsController(service as any); + + it("create envelope döner", async () => { + const r = await c.create({ name: "n", description: "d", tags: [], graph: { nodes: [], edges: [] } } as any); + expect(r).toEqual({ success: true, data: { id: "1" } }); + }); + it("search default k/minScore geçirir", async () => { + await c.search({ query: "x" } as any); + expect(service.search).toHaveBeenCalledWith("x", expect.any(Number), expect.any(Number)); + }); +}); +``` + +- [ ] **Step 6: Test + build + commit** + +Run: `pnpm vitest run src/patterns && pnpm build` +Expected: PASS + build temiz. + +```bash +git add src/patterns/ src/app.module.ts +git commit -m "feat(patterns): controller + DTO + modül (CRUD + search + promote)" +``` + +--- + +### Task 9: Seed — kanonik pattern'ler + +**Files:** +- Create: `src/patterns/seed/canonical-patterns.ts`, `src/patterns/seed/seed.ts` +- Modify: `package.json` (scripts) + +- [ ] **Step 1: Kanonik pattern verisi** + +`src/patterns/seed/canonical-patterns.ts` — `CreatePatternInput[]` (en az 12). Her biri enriched v4 +node şemalarına uymalı. Örnek 2 giriş (kalanları aynı yapıda ekle: JWT auth akışı, katmanlı CRUD, +Saga ödeme, CQRS read model, cache-aside, event-driven handler, API gateway routing, +repository+custom query, DTO validation katmanı, exception hiyerarşisi, worker/cron, external service entegrasyonu): + +```ts +import type { CreatePatternInput } from "../schemas/pattern.schema"; + +export const CANONICAL_PATTERNS: CreatePatternInput[] = [ + { + name: "Katmanlı CRUD (Controller→Service→Repository→Table)", + description: "Standart REST CRUD: Controller HTTP isteğini alır, Service iş mantığını çalıştırır, Repository veriye erişir, Table veriyi tutar. En yaygın backend katmanlı mimari.", + tags: ["crud", "layered", "rest", "backend"], + graph: { + nodes: [ + { tempId: "t_ctrl", type: "Controller", properties: { ControllerName: "ResourceController", Description: "Kaynak REST API", BaseRoute: "/api/v1/resources", Endpoints: [{ HttpMethod: "POST", Route: "/", RequestDTORef: "CreateResourceDTO", ResponseDTORef: "ResourceDTO", RequiresAuth: true }] } }, + { tempId: "t_svc", type: "Service", properties: { ServiceName: "ResourceService", Description: "Kaynak iş mantığı", IsTransactionScoped: true, Methods: [{ MethodName: "create", Parameters: [{ Name: "dto", Type: "CreateResourceDTO", DtoRef: "CreateResourceDTO" }], ReturnType: "ResourceDTO", ReturnDtoRef: "ResourceDTO", IsAsync: true }], Dependencies: [{ Kind: "Repository", Ref: "ResourceRepository" }] } }, + { tempId: "t_repo", type: "Repository", properties: { RepositoryName: "ResourceRepository", Description: "Kaynak veri erişimi", EntityReference: "resources", IsCached: false, CustomQueries: [] } }, + { tempId: "t_tbl", type: "Table", properties: { TableName: "resources", Description: "Kaynak tablosu", Columns: [{ Name: "id", DataType: "UUID", IsPrimaryKey: true, IsNotNull: true, IsUnique: true, AutoIncrement: false }] } }, + { tempId: "t_dto", type: "DTO", properties: { Name: "CreateResourceDTO", Description: "Kaynak oluşturma isteği", Fields: [{ Name: "name", DataType: "string", IsRequired: true, IsArray: false, ValidationRules: [{ Rule: "MinLength", Value: "1" }] }] } }, + ], + edges: [ + { sourceTempId: "t_ctrl", targetTempId: "t_svc", edgeType: "CALLS" }, + { sourceTempId: "t_svc", targetTempId: "t_repo", edgeType: "CALLS" }, + { sourceTempId: "t_repo", targetTempId: "t_tbl", edgeType: "WRITES" }, + { sourceTempId: "t_ctrl", targetTempId: "t_dto", edgeType: "USES" }, + ], + }, + }, + { + name: "Cache-aside (Service→Cache + Service→Repository)", + description: "Okuma yükünü azaltmak için cache-aside deseni: Service önce Cache'e bakar, yoksa Repository'den okur ve Cache'e yazar. Redis TTL'li.", + tags: ["cache", "performance", "read-heavy", "redis"], + graph: { + nodes: [ + { tempId: "t_svc", type: "Service", properties: { ServiceName: "ProfileService", Description: "Profil okuma", IsTransactionScoped: false, Methods: [{ MethodName: "getProfile", Parameters: [{ Name: "id", Type: "UUID" }], ReturnType: "ProfileDTO", IsAsync: true }], Dependencies: [{ Kind: "Cache", Ref: "ProfileCache" }, { Kind: "Repository", Ref: "ProfileRepository" }] } }, + { tempId: "t_cache", type: "Cache", properties: { CacheName: "ProfileCache", Description: "Profil cache", KeyPattern: "profile:{id}", TTL_Seconds: 3600, Engine: "Redis", EvictionPolicy: "LRU" } }, + { tempId: "t_repo", type: "Repository", properties: { RepositoryName: "ProfileRepository", Description: "Profil veri erişimi", EntityReference: "profiles", IsCached: true, CustomQueries: [] } }, + ], + edges: [ + { sourceTempId: "t_svc", targetTempId: "t_cache", edgeType: "CACHES_IN" }, + { sourceTempId: "t_svc", targetTempId: "t_repo", edgeType: "CALLS" }, + ], + }, + }, + // … kalan 10 pattern aynı yapıda (yukarıdaki listeyi tamamla) +]; +``` + +> **Not:** Edge `edgeType` değerleri mevcut `EdgeKindSchema`'ya uymalı (CALLS/WRITES/QUERIES/USES/CACHES_IN/PUBLISHES/SUBSCRIBES/ROUTES_TO/HAS/RETURNS/EXTENDS/THROWS/DEPENDS_ON/READS_CONFIG vb). Node properties enriched v4 şemalarına uymalı — emin değilsen `GET /api/v1/node-types/:id` şemasına bak. + +- [ ] **Step 2: Seed runner (idempotent)** + +`src/patterns/seed/seed.ts`: + +```ts +import { Neo4jService } from "../../neo4j/neo4j.service"; +import { ProjectsRepository } from "../../projects/projects.repository"; +import { PatternsRepository } from "../patterns.repository"; +import { PatternsService } from "../patterns.service"; +import { EmbeddingsService } from "../../embeddings/embeddings.service"; +import { CANONICAL_PATTERNS } from "./canonical-patterns"; +import { env } from "../../config/env"; + +async function main(): Promise { + const neo4j = new Neo4jService({ uri: env.NEO4J_URI, user: env.NEO4J_USER, password: env.NEO4J_PASSWORD }); + await neo4j.onModuleInit(); + const repo = new PatternsRepository(neo4j); + const svc = new PatternsService(repo, new ProjectsRepository(neo4j), new EmbeddingsService()); + + let created = 0, skipped = 0; + for (const p of CANONICAL_PATTERNS) { + if (await repo.findByName(p.name)) { skipped++; continue; } + await svc.create(p, "seed"); + created++; + } + await neo4j.onModuleDestroy(); + console.log(`✓ Pattern seed: ${created} eklendi, ${skipped} atlandı (mevcut).`); +} + +main().catch((e) => { console.error("✗ Seed failed:", e); process.exit(1); }); +``` + +- [ ] **Step 3: package.json script** + +```json +"seed:patterns": "tsx --env-file=.env src/patterns/seed/seed.ts" +``` + +- [ ] **Step 4: Seed çalıştır (Neo4j + vektör index + Bedrock açık)** + +Run: `pnpm migrate:patterns-index && pnpm seed:patterns` +Expected: `✓ Pattern seed: 12 eklendi, 0 atlandı.` (ikinci çalıştırmada hepsi atlanır → idempotent) + +Doğrula: `curl -s http://localhost:4000/api/v1/patterns | python3 -c "import sys,json;print(len(json.load(sys.stdin)['data']),'pattern')"` + +- [ ] **Step 5: Commit** + +```bash +git add src/patterns/seed/ package.json +git commit -m "feat(patterns): seed — 12 kanonik mimari pattern (enriched v4)" +``` + +--- + +### Task 10: AI retrieval entegrasyonu + prompt enjeksiyonu + +**Files:** +- Modify: `src/ai/prompts/system-prompt.ts`, `src/ai/ai.service.ts`, `src/ai/ai.module.ts` +- Test: `src/ai/prompts/system-prompt.spec.ts` (yeni) + +- [ ] **Step 1: buildSystemPrompt'a patterns parametresi** + +`src/ai/prompts/system-prompt.ts` — `buildSystemPrompt` imzasını genişlet: + +```ts +import type { PatternSearchHit } from "../../patterns/patterns.repository"; + +export function buildSystemPrompt(graph: ProjectGraph, patterns: PatternSearchHit[] = []): string { + // … mevcut nodeSummary/edgeSummary … + + const patternBlock = patterns.length === 0 ? "" : ` + +## İLGİLİ REFERANS DESENLER (retrieval) +Aşağıdaki kanıtlanmış mimari desenler isteğine yakın. Uygunsa bunlara benzeterek üret (birebir kopyalama; uyarlayıp gerekeni ekle): +${patterns.map((h) => { + const g = h.pattern.graph; + const nodeTypes = g.nodes.map((n) => n.type).join(", "); + const edgeKinds = g.edges.map((e) => e.edgeType).join(", "); + return `- **${h.pattern.name}** (benzerlik ${h.score.toFixed(2)}): ${h.pattern.description}\n Yapı: ${g.nodes.length} node [${nodeTypes}], edge'ler [${edgeKinds || "yok"}]`; +}).join("\n")}`; + + return `${BASE_PROMPT} +${patternBlock} + +## MEVCUT KANVAS DURUMU (current_graph) +…`; // mevcut current_graph bölümü patternBlock'tan SONRA gelir +} +``` + +(Mevcut `return` template'ini koru; sadece `${patternBlock}` satırını BASE_PROMPT ile current_graph arasına ekle.) + +- [ ] **Step 2: Failing test** + +`src/ai/prompts/system-prompt.spec.ts`: + +```ts +import { describe, it, expect } from "vitest"; +import { buildSystemPrompt } from "./system-prompt"; + +const emptyGraph = { project: {} as any, nodes: [], edges: [], counts: { nodes: 0, edges: 0 } }; + +describe("buildSystemPrompt patterns", () => { + it("pattern yoksa REFERANS DESENLER bölümü yok", () => { + expect(buildSystemPrompt(emptyGraph as any)).not.toContain("REFERANS DESENLER"); + }); + it("pattern varsa isim + skor + yapı enjekte eder", () => { + const hits = [{ score: 0.88, pattern: { name: "Katmanlı CRUD", description: "açıklama", graph: { nodes: [{ tempId: "t", type: "Controller", properties: {} }], edges: [] } } }]; + const p = buildSystemPrompt(emptyGraph as any, hits as any); + expect(p).toContain("REFERANS DESENLER"); + expect(p).toContain("Katmanlı CRUD"); + expect(p).toContain("0.88"); + expect(p).toContain("Controller"); + }); +}); +``` + +- [ ] **Step 3: Test fail → Step 1'i uygula → pass** + +Run: `pnpm vitest run src/ai/prompts/system-prompt.spec.ts` +Expected: PASS (2 test). + +- [ ] **Step 4: ai.service retrieval enjeksiyonu** + +`src/ai/ai.service.ts` — `PatternsService` enjekte et, `chat` içinde current_graph yüklendikten sonra: + +```ts +// constructor'a ekle: private readonly patterns: PatternsService, +// chat() içinde, buildSystemPrompt çağrısından ÖNCE: +let patternHits: PatternSearchHit[] = []; +try { + patternHits = await this.patterns.search(body.message, env.EMBED_TOP_K, env.EMBED_MIN_SCORE); +} catch (e) { + this.logger.warn(`Pattern retrieval atlandı: ${(e as Error).message}`); +} +const systemPrompt = buildSystemPrompt({ project, nodes, edges, counts: { nodes: nodes.length, edges: edges.length } }, patternHits); +``` + +(import: `PatternsService`, `PatternSearchHit`, `env`. Mevcut `buildSystemPrompt` çağrısını bununla değiştir.) + +- [ ] **Step 5: ai.module.ts'e PatternsModule ekle** + +`src/ai/ai.module.ts` `imports`'a `PatternsModule` ekle (PatternsService inject edilebilsin). + +- [ ] **Step 6: Test + build + commit** + +Run: `pnpm vitest run src/ai && pnpm build` +Expected: PASS + build temiz. + +```bash +git add src/ai/ +git commit -m "feat(ai): GraphRAG — retrieval edilen pattern'leri system prompt'a enjekte et" +``` + +--- + +### Task 11: E2E — fake embedder + vektör index round-trip + +**Files:** +- Create: `test/patterns.e2e-spec.ts` +- Referans: `test/nodes.e2e-spec.ts` (Test.createTestingModule + overrideProvider deseni) + +- [ ] **Step 1: Fake embedder + e2e** + +`test/patterns.e2e-spec.ts` — gerçek embedding API'ye bağlanmadan vektör index Cypher'ını doğrular. +Deterministik fake embedder: metni `EMBED_DIM` boyutlu sabit vektöre map'ler. + +```ts +import { Test } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import request from "supertest"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { AppModule } from "../src/app.module"; +import { EMBEDDINGS } from "../src/embeddings/embeddings.types"; +import { Neo4jService } from "../src/neo4j/neo4j.service"; +import { env } from "../src/config/env"; + +// Deterministik fake: char-code toplamını dim boyutuna yayar, normalize eder. +function fakeVec(text: string): number[] { + const dim = env.EMBED_DIM; + const v = new Array(dim).fill(0); + for (let i = 0; i < text.length; i++) v[i % dim] += text.charCodeAt(i); + const norm = Math.sqrt(v.reduce((s, x) => s + x * x, 0)) || 1; + return v.map((x) => x / norm); +} +const fakeEmbeddings = { + isConfigured: () => true, + embed: async (t: string) => fakeVec(t), + embedBatch: async (ts: string[]) => ts.map(fakeVec), +}; + +describe("Patterns E2E", () => { + let app: INestApplication; + let neo4j: Neo4jService; + + beforeAll(async () => { + const mod = await Test.createTestingModule({ imports: [AppModule] }) + .overrideProvider(EMBEDDINGS).useValue(fakeEmbeddings) + .compile(); + app = mod.createNestApplication(); + await app.init(); + neo4j = app.get(Neo4jService); + // vektör index (idempotent) + await neo4j.run( + `CREATE VECTOR INDEX pattern_embedding IF NOT EXISTS FOR (p:Pattern) ON (p.embedding) + OPTIONS { indexConfig: { \`vector.dimensions\`: $dim, \`vector.similarity_function\`: 'cosine' } }`, + { dim: env.EMBED_DIM }, + ); + await neo4j.run(`MATCH (p:Pattern) DELETE p`); // temiz başlangıç + }); + + afterAll(async () => { + await neo4j.run(`MATCH (p:Pattern) DELETE p`); + await app.close(); + }); + + const base = "/api/v1"; + const graph = { nodes: [{ tempId: "t_ctrl", type: "Controller", properties: { ControllerName: "X", Description: "d", BaseRoute: "/x", Endpoints: [{ HttpMethod: "GET", Route: "/", RequiresAuth: false }] } }], edges: [] }; + + it("create → list → search round-trip", async () => { + const create = await request(app.getHttpServer()).post(`${base}/patterns`) + .send({ name: "Auth akışı", description: "JWT login authentication", tags: ["auth"], graph }).expect(201); + expect(create.body.data.name).toBe("Auth akışı"); + + await request(app.getHttpServer()).get(`${base}/patterns`).expect(200) + .then((r) => expect(r.body.data.length).toBeGreaterThanOrEqual(1)); + + // vektör index'in indekslemesi için kısa bekleme (Neo4j eventual) + await new Promise((r) => setTimeout(r, 1500)); + const search = await request(app.getHttpServer()).post(`${base}/patterns/search`) + .send({ query: "JWT login authentication", k: 5, minScore: 0 }).expect(200); + expect(search.body.data.length).toBeGreaterThanOrEqual(1); + expect(search.body.data[0].pattern.name).toBe("Auth akışı"); + expect(search.body.data[0].score).toBeGreaterThan(0); + }); + + it("getById olmayan → 404", async () => { + await request(app.getHttpServer()).get(`${base}/patterns/00000000-0000-0000-0000-000000000000`).expect(404); + }); +}); +``` + +- [ ] **Step 2: E2E çalıştır** + +Run: `pnpm test:e2e` +Expected: 2 patterns e2e + mevcut 9 nodes e2e = PASS. (Testcontainers Neo4j 5-community vektör index destekler.) + +> Eğer vektör index Testcontainers imajında yoksa: `docker-compose.yml`'deki `neo4j:5-community` zaten 5.x; e2e Testcontainers de `neo4j:5-community` kullanmalı (`test/` setup'ını kontrol et, gerekirse imaj tag'ini eşitle). + +- [ ] **Step 3: Commit** + +```bash +git add test/patterns.e2e-spec.ts +git commit -m "test(patterns): e2e — fake embedder + vektör index search round-trip" +``` + +--- + +### Task 12: Canlı doğrulama + push + +**Files:** yok (doğrulama). + +- [ ] **Step 1: Tam test paketi** + +Run: `pnpm test` +Expected: tüm unit PASS (env.spec dahil). + +Run: `pnpm test:e2e` +Expected: tüm e2e PASS. + +- [ ] **Step 2: Server + gerçek Bedrock embeddings ile canlı** + +```bash +pnpm build +pnpm migrate:patterns-index +pnpm seed:patterns # gerçek Bedrock ile ~12 pattern embed +set -a; source .env; set +a +PORT=4000 node dist/main.js & +# 1) semantik arama: +curl -s -X POST localhost:4000/api/v1/patterns/search -H 'Content-Type: application/json' \ + -d '{"query":"kullanıcı CRUD API katmanlı"}' | python3 -m json.tool +# 2) agent retrieval: proje oluştur + chat; system prompt'a pattern enjekte mi? +``` + +Expected: +- Search, "Katmanlı CRUD" pattern'ini yüksek skorla döner. +- AI chat isteğinde ilgili pattern enjekte edilir; agent benzer yapı üretir (loglardan/çıktıdan doğrula). + +- [ ] **Step 3: Memory + push** + +Memory dosyası `project_solarch_backend_phase1.md`'ye Phase 4 özeti ekle (pattern lib + vector index + embeddings + retrieval; embedding model+dim Task 1 sonucu; gotchas). + +```bash +git push origin main +``` + +--- + +## Faz Çıkış Kriterleri (Spec ile) + +- [ ] Embedding endpoint + model + dim doğrulandı (Task 1) → env (Task 2) +- [ ] EmbeddingsService provider-abstracted, degrade ediyor (Task 3) +- [ ] Pattern Zod şeması = apply formatı (Task 4) +- [ ] Vektör index migration, env EMBED_DIM (Task 5) +- [ ] Repository native vektör arama (Task 6) +- [ ] Service: embed+store, search (degrade), promote (Task 7) +- [ ] CRUD + search + promote endpoint'leri (Task 8) +- [ ] ~12 kanonik pattern seed, idempotent (Task 9) +- [ ] Retrieval system prompt'a otomatik enjekte (Task 10) +- [ ] E2E fake embedder + vektör index (Task 11) +- [ ] Canlı: gerçek Bedrock embed + agent enjeksiyon (Task 12) + +## Notlar + +- **Task 1 İLK yapılmalı** — embedding boyutu vektör index'i (Task 5) ve fake embedder'ı (Task 11) belirler. Mantle embeddings sunmuyorsa local'e (Xenova, dim=384) düş; tüm dim referansları env.EMBED_DIM üzerinden olduğu için kod değişmez, sadece env default'u + Task 3 factory branch'i. +- `graphJson` = `GraphService.apply` girdi formatı → ileride "pattern'i doğrudan apply et" bedavaya gelir (Phase 5). +- Retrieval bir *enhancement*: embedding/index/sonuç yoksa agent normal çalışır (degrade, asla hata fırlatmaz — sadece create explicit 503). +- Neo4j vektör index **eventual** indeksler; e2e'de kısa bekleme gerekebilir (Task 11 Step 1). diff --git a/apps/server/docs/plans/2026-05-22-tabs-contexts-implementation.md b/apps/server/docs/plans/2026-05-22-tabs-contexts-implementation.md new file mode 100644 index 0000000..4d2359c --- /dev/null +++ b/apps/server/docs/plans/2026-05-22-tabs-contexts-implementation.md @@ -0,0 +1,1134 @@ +# Tabs / Contexts Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Proje içinde çoklu sekme (context) + kutuların sekmeler arası referans (import) ile paylaşımı. + +**Architecture:** "Tek ev + referans" modeli. Node tek ev sekmesinde tanımlı (`positionX/Y` korunur + `homeTabId` eklenir). Başka sekmelerde `(:Tab)-[:REFERENCES {x,y}]->(:Node)` ilişkisiyle import. Çoğunlukla additive — node.position korunduğundan apply/AI/CRUD/graph bozulmaz. + +**Tech Stack:** NestJS 11, Neo4j 5 (raw Cypher), Zod + nestjs-zod, Vitest + Testcontainers. + +**Modül bağımlılık yönü (döngü önleme):** `TabsRepository` SADECE `Neo4jService`'e bağlanır; proje/node varlığını kendi Cypher'ıyla kontrol eder. Böylece `ProjectsModule → TabsModule` ve `NodesModule → TabsModule` tek yönlüdür (TabsModule, Projects/Nodes'tan hiçbir şey import etmez). + +**Referans desenler:** `src/projects/projects.repository.ts` (Cypher repo + toStoredX), `src/nodes/nodes.service.ts` (create + existence check), `src/edges/edges.repository.ts` (APOC ilişki), `src/common/envelope.ts` (`ok()`), `src/node-types/node-types.controller.ts` (Scalar decorator), `test/patterns.e2e-spec.ts` (Testcontainers + overrideProvider deseni), `src/neo4j/migrations/data/004-pattern-vector-index.ts` (TS migration). + +--- + +## File Structure + +- `src/tabs/schemas/tab.schema.ts` — Zod: Tab, CreateTab, UpdateTab, Reference, Layout + tipler +- `src/tabs/dto/*.dto.ts` — createZodDto sınıfları +- `src/tabs/tabs.repository.ts` — Cypher: tab CRUD + reference + tabGraph + layout + existence +- `src/tabs/tabs.service.ts` — iş kuralları (default koruması, self-ref reddi, home taşıma) +- `src/tabs/tabs.controller.ts` — endpoint'ler +- `src/tabs/tabs.module.ts` — modül (imports: [Neo4jModule]; exports: [TabsService, TabsRepository]) +- `src/neo4j/migrations/data/005-tabs.ts` — default tab + homeTabId backfill +- `src/neo4j/migrations/002_tab_constraint.cypher` — tab_id_unique +- Modify: `src/nodes/schemas/base.schema.ts` (BaseNodeSchema + homeTabId), `src/nodes/dto/create-node.dto.ts` (CreatableBaseFields + homeTabId), `src/nodes/nodes.repository.ts` (StoredNode + homeTabId), `src/nodes/nodes.service.ts` (default tab), `src/nodes/nodes.module.ts` (import TabsModule) +- Modify: `src/projects/projects.service.ts` + `projects.module.ts` (create → default tab), `src/projects/projects.repository.ts` (nodeFromRecord homeTabId) +- Modify: `src/graph/dto/apply-graph.dto.ts` + `graph.service.ts` + `graph.module.ts` (tabId) +- Modify: `src/ai/ai.service.ts` + `dto/chat.dto.ts` + `ai.module.ts` (tabId) +- Modify: `src/app.module.ts` (TabsModule), `package.json` (migrate:tabs script) +- Test: `test/tabs.e2e-spec.ts` + +--- + +### Task 1: Tab Zod şeması + DTO'lar + +**Files:** +- Create: `src/tabs/schemas/tab.schema.ts`, `src/tabs/dto/create-tab.dto.ts`, `update-tab.dto.ts`, `reference.dto.ts`, `layout.dto.ts` +- Test: `src/tabs/schemas/tab.schema.spec.ts` + +- [ ] **Step 1: Şema** + +`src/tabs/schemas/tab.schema.ts`: + +```ts +import { z } from "zod"; + +export const CreateTabSchema = z.object({ + name: z.string().min(1), + moduleNodeId: z.string().uuid().optional(), +}).strict(); + +export const UpdateTabSchema = z.object({ + name: z.string().min(1).optional(), + order: z.number().int().nonnegative().optional(), +}).strict(); + +export const ReferenceSchema = z.object({ + x: z.number(), + y: z.number(), +}).strict(); + +export const LayoutSchema = z.object({ + items: z.array(z.object({ + nodeId: z.string().uuid(), + x: z.number(), + y: z.number(), + }).strict()).min(1), +}).strict(); + +export type CreateTabInput = z.infer; +export type UpdateTabInput = z.infer; +export type ReferenceInput = z.infer; +export type LayoutInput = z.infer; + +export interface StoredTab { + id: string; + projectId: string; + name: string; + isDefault: boolean; + order: number; + moduleNodeId?: string; + createdAt: string; + updatedAt: string; +} + +/** Bir sekmenin render içeriği: pozisyonlu node'lar + aralarındaki edge'ler. */ +export interface TabGraphMember { + id: string; + type: string; + properties: Record; + position: { x: number; y: number }; + isReference: boolean; + origin?: string; // referans ise node'un ev sekmesi (homeTabId) +} +export interface TabGraphEdge { + id: string; + kind: string; + sourceNodeId: string; + targetNodeId: string; +} +export interface TabGraph { + tab: StoredTab; + nodes: TabGraphMember[]; + edges: TabGraphEdge[]; +} +``` + +- [ ] **Step 2: DTO'lar** + +`create-tab.dto.ts`: +```ts +import { createZodDto } from "nestjs-zod"; +import { CreateTabSchema } from "../schemas/tab.schema"; +export class CreateTabDto extends createZodDto(CreateTabSchema) {} +``` +`update-tab.dto.ts`: +```ts +import { createZodDto } from "nestjs-zod"; +import { UpdateTabSchema } from "../schemas/tab.schema"; +export class UpdateTabDto extends createZodDto(UpdateTabSchema) {} +``` +`reference.dto.ts`: +```ts +import { createZodDto } from "nestjs-zod"; +import { ReferenceSchema } from "../schemas/tab.schema"; +export class ReferenceDto extends createZodDto(ReferenceSchema) {} +``` +`layout.dto.ts`: +```ts +import { createZodDto } from "nestjs-zod"; +import { LayoutSchema } from "../schemas/tab.schema"; +export class LayoutDto extends createZodDto(LayoutSchema) {} +``` + +- [ ] **Step 3: Failing test** + +`src/tabs/schemas/tab.schema.spec.ts`: +```ts +import { describe, it, expect } from "vitest"; +import { CreateTabSchema, LayoutSchema } from "./tab.schema"; + +describe("Tab schemas", () => { + it("CreateTab geçerli", () => { + expect(CreateTabSchema.parse({ name: "Sipariş Modülü" }).name).toBe("Sipariş Modülü"); + }); + it("CreateTab boş isim reddeder", () => { + expect(() => CreateTabSchema.parse({ name: "" })).toThrow(); + }); + it("Layout boş items reddeder", () => { + expect(() => LayoutSchema.parse({ items: [] })).toThrow(); + }); + it("Layout geçerli item kabul eder", () => { + expect(LayoutSchema.parse({ items: [{ nodeId: "550e8400-e29b-41d4-a716-446655440000", x: 1, y: 2 }] }).items).toHaveLength(1); + }); +}); +``` + +- [ ] **Step 4: Test pass + commit** + +Run: `pnpm vitest run src/tabs/schemas` +Expected: PASS (4 test). +```bash +git add src/tabs/schemas src/tabs/dto +git commit -m "feat(tabs): Zod şema + DTO (CreateTab/UpdateTab/Reference/Layout)" +``` + +--- + +### Task 2: TabsRepository (Cypher) + +**Files:** +- Create: `src/tabs/tabs.repository.ts` +- Test: `src/tabs/tabs.repository.spec.ts` + +- [ ] **Step 1: Repository** + +`src/tabs/tabs.repository.ts`: + +```ts +import { Injectable } from "@nestjs/common"; +import { Neo4jService } from "../neo4j/neo4j.service"; +import type { StoredTab, TabGraph, TabGraphMember, TabGraphEdge } from "./schemas/tab.schema"; + +@Injectable() +export class TabsRepository { + constructor(private readonly neo4j: Neo4jService) {} + + async projectExists(projectId: string): Promise { + const r = await this.neo4j.run(`MATCH (p:Project {id: $projectId}) RETURN p LIMIT 1`, { projectId }); + return r.records.length > 0; + } + + async nodeExists(projectId: string, nodeId: string): Promise { + const r = await this.neo4j.run( + `MATCH (n:Node {id: $nodeId, projectId: $projectId}) RETURN n LIMIT 1`, + { projectId, nodeId }, + ); + return r.records.length > 0; + } + + async create(tab: StoredTab): Promise { + await this.neo4j.run( + `CREATE (t:Tab { + id: $id, projectId: $projectId, name: $name, isDefault: $isDefault, + order: $order, moduleNodeId: $moduleNodeId, + createdAt: datetime($createdAt), updatedAt: datetime($updatedAt) + })`, + { ...tab, moduleNodeId: tab.moduleNodeId ?? null }, + ); + } + + async list(projectId: string): Promise { + const r = await this.neo4j.run( + `MATCH (t:Tab {projectId: $projectId}) RETURN t ORDER BY t.order ASC`, + { projectId }, + ); + return r.records.map((rec) => toStoredTab(rec.get("t").properties)); + } + + async getById(projectId: string, tabId: string): Promise { + const r = await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId}) RETURN t`, + { projectId, tabId }, + ); + return r.records.length ? toStoredTab(r.records[0].get("t").properties) : null; + } + + async findDefault(projectId: string): Promise { + const r = await this.neo4j.run( + `MATCH (t:Tab {projectId: $projectId, isDefault: true}) RETURN t LIMIT 1`, + { projectId }, + ); + return r.records.length ? toStoredTab(r.records[0].get("t").properties) : null; + } + + async maxOrder(projectId: string): Promise { + const r = await this.neo4j.run( + `MATCH (t:Tab {projectId: $projectId}) RETURN coalesce(max(t.order), -1) AS m`, + { projectId }, + ); + return Number(r.records[0].get("m")); + } + + async update(projectId: string, tabId: string, patch: { name?: string; order?: number; updatedAt: string }): Promise { + const sets: string[] = ["t.updatedAt = datetime($updatedAt)"]; + if (patch.name !== undefined) sets.push("t.name = $name"); + if (patch.order !== undefined) sets.push("t.order = $order"); + const r = await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId}) SET ${sets.join(", ")} RETURN t`, + { projectId, tabId, name: patch.name ?? null, order: patch.order ?? null, updatedAt: patch.updatedAt }, + ); + return r.records.length ? toStoredTab(r.records[0].get("t").properties) : null; + } + + /** Sekmeyi sil: owned node'ların evini default'a taşı, REFERENCES'larını kaldır, tab'ı sil. */ + async deleteAndReassign(projectId: string, tabId: string, defaultTabId: string): Promise { + await this.neo4j.run( + `MATCH (n:Node {projectId: $projectId, homeTabId: $tabId}) SET n.homeTabId = $defaultTabId`, + { projectId, tabId, defaultTabId }, + ); + await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId})-[r:REFERENCES]->() DELETE r`, + { projectId, tabId }, + ); + await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId}) DELETE t`, + { projectId, tabId }, + ); + } + + /** Referans ekle/güncelle (upsert). */ + async upsertReference(projectId: string, tabId: string, nodeId: string, x: number, y: number): Promise { + await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId}) + MATCH (n:Node {id: $nodeId, projectId: $projectId}) + MERGE (t)-[r:REFERENCES]->(n) + SET r.x = $x, r.y = $y`, + { projectId, tabId, nodeId, x, y }, + ); + } + + async removeReference(projectId: string, tabId: string, nodeId: string): Promise { + const r = await this.neo4j.run( + `MATCH (t:Tab {id: $tabId, projectId: $projectId})-[r:REFERENCES]->(n:Node {id: $nodeId}) + DELETE r RETURN 1 AS d`, + { projectId, tabId, nodeId }, + ); + return r.records.length > 0; + } + + /** Toplu layout kaydet: owned → node.positionX/Y, referenced → REFERENCES.x/y. */ + async saveLayout(projectId: string, tabId: string, items: { nodeId: string; x: number; y: number }[]): Promise { + await this.neo4j.run( + `UNWIND $items AS item + MATCH (n:Node {id: item.nodeId, projectId: $projectId}) + FOREACH (_ IN CASE WHEN n.homeTabId = $tabId THEN [1] ELSE [] END | + SET n.positionX = item.x, n.positionY = item.y) + WITH n, item + OPTIONAL MATCH (t:Tab {id: $tabId, projectId: $projectId})-[r:REFERENCES]->(n) + FOREACH (_ IN CASE WHEN r IS NOT NULL THEN [1] ELSE [] END | + SET r.x = item.x, r.y = item.y)`, + { projectId, tabId, items }, + ); + } + + /** Sekmenin render içeriği: owned (homeTabId=tab) + referenced node'lar + iki ucu da + * görünen edge'ler. */ + async tabGraph(projectId: string, tab: StoredTab): Promise { + const ownedRes = await this.neo4j.run( + `MATCH (n:Node {projectId: $projectId, homeTabId: $tabId}) RETURN n, labels(n) AS labels`, + { projectId, tabId: tab.id }, + ); + const owned: TabGraphMember[] = ownedRes.records.map((rec) => + memberFrom(rec.get("n").properties, rec.get("labels"), false), + ); + + const refRes = await this.neo4j.run( + `MATCH (:Tab {id: $tabId, projectId: $projectId})-[r:REFERENCES]->(n:Node) + RETURN n, labels(n) AS labels, r.x AS x, r.y AS y`, + { projectId, tabId: tab.id }, + ); + const referenced: TabGraphMember[] = refRes.records.map((rec) => { + const m = memberFrom(rec.get("n").properties, rec.get("labels"), true); + m.position = { x: Number(rec.get("x")), y: Number(rec.get("y")) }; + return m; + }); + + const members = [...owned, ...referenced]; + const visibleIds = new Set(members.map((m) => m.id)); + + const edgesRes = await this.neo4j.run( + `MATCH (s:Node)-[e]->(t:Node) + WHERE e.projectId = $projectId AND s.id IN $ids AND t.id IN $ids + RETURN e.id AS id, type(e) AS kind, s.id AS sourceNodeId, t.id AS targetNodeId`, + { projectId, ids: [...visibleIds] }, + ); + const edges: TabGraphEdge[] = edgesRes.records.map((rec) => ({ + id: rec.get("id"), + kind: rec.get("kind"), + sourceNodeId: rec.get("sourceNodeId"), + targetNodeId: rec.get("targetNodeId"), + })); + + return { tab, nodes: members, edges }; + } +} + +function toStoredTab(p: any): StoredTab { + return { + id: p.id, + projectId: p.projectId, + name: p.name, + isDefault: p.isDefault, + order: Number(p.order), + moduleNodeId: p.moduleNodeId ?? undefined, + createdAt: new Date(p.createdAt).toISOString(), + updatedAt: new Date(p.updatedAt).toISOString(), + }; +} + +function memberFrom(p: any, labels: string[], isReference: boolean): TabGraphMember { + const kind = labels.find((l: string) => l !== "Node") as string; + return { + id: p.id, + type: kind, + properties: JSON.parse(p.properties), + position: { x: Number(p.positionX), y: Number(p.positionY) }, + isReference, + origin: isReference ? p.homeTabId : undefined, + }; +} +``` + +- [ ] **Step 2: Failing test (mock Neo4jService)** + +`src/tabs/tabs.repository.spec.ts`: +```ts +import { describe, it, expect, vi } from "vitest"; +import { TabsRepository } from "./tabs.repository"; + +const neo4j = { run: vi.fn() }; +const repo = new TabsRepository(neo4j as any); + +describe("TabsRepository", () => { + it("upsertReference MERGE kullanır", async () => { + neo4j.run.mockResolvedValueOnce({ records: [] }); + await repo.upsertReference("p", "t", "n", 10, 20); + expect(neo4j.run.mock.calls[0][0]).toContain("MERGE (t)-[r:REFERENCES]->(n)"); + expect(neo4j.run.mock.calls[0][1]).toMatchObject({ projectId: "p", tabId: "t", nodeId: "n", x: 10, y: 20 }); + }); + + it("findDefault isDefault:true filtreler", async () => { + neo4j.run.mockResolvedValueOnce({ records: [] }); + expect(await repo.findDefault("p")).toBeNull(); + expect(neo4j.run.mock.calls[0][0]).toContain("isDefault: true"); + }); + + it("removeReference yoksa false", async () => { + neo4j.run.mockResolvedValueOnce({ records: [] }); + expect(await repo.removeReference("p", "t", "n")).toBe(false); + }); +}); +``` + +- [ ] **Step 3: Test pass + commit** + +Run: `pnpm vitest run src/tabs/tabs.repository.spec.ts` +Expected: PASS (3 test). +```bash +git add src/tabs/tabs.repository.ts src/tabs/tabs.repository.spec.ts +git commit -m "feat(tabs): repository — tab CRUD + REFERENCES upsert + tabGraph + layout" +``` + +--- + +### Task 3: TabsService (iş kuralları) + +**Files:** +- Create: `src/tabs/tabs.service.ts` +- Test: `src/tabs/tabs.service.spec.ts` + +- [ ] **Step 1: Service** + +`src/tabs/tabs.service.ts`: + +```ts +import { Injectable, BadRequestException, NotFoundException } from "@nestjs/common"; +import { randomUUID } from "node:crypto"; +import { TabsRepository } from "./tabs.repository"; +import type { StoredTab, TabGraph, CreateTabInput, UpdateTabInput } from "./schemas/tab.schema"; + +@Injectable() +export class TabsService { + constructor(private readonly repo: TabsRepository) {} + + /** Projenin default ("Ana Mimari") sekmesi — yoksa oluşturur (idempotent). */ + async ensureDefault(projectId: string): Promise { + const existing = await this.repo.findDefault(projectId); + if (existing) return existing; + const now = new Date().toISOString(); + const tab: StoredTab = { + id: randomUUID(), projectId, name: "Ana Mimari", + isDefault: true, order: 0, createdAt: now, updatedAt: now, + }; + await this.repo.create(tab); + return tab; + } + + async create(projectId: string, input: CreateTabInput): Promise { + await this.assertProject(projectId); + const order = (await this.repo.maxOrder(projectId)) + 1; + const now = new Date().toISOString(); + const tab: StoredTab = { + id: randomUUID(), projectId, name: input.name, + isDefault: false, order, moduleNodeId: input.moduleNodeId, + createdAt: now, updatedAt: now, + }; + await this.repo.create(tab); + return tab; + } + + async list(projectId: string): Promise { + await this.assertProject(projectId); + return this.repo.list(projectId); + } + + async getById(projectId: string, tabId: string): Promise { + const tab = await this.repo.getById(projectId, tabId); + if (!tab) throw this.tabNotFound(tabId); + return tab; + } + + async update(projectId: string, tabId: string, input: UpdateTabInput): Promise { + const updated = await this.repo.update(projectId, tabId, { ...input, updatedAt: new Date().toISOString() }); + if (!updated) throw this.tabNotFound(tabId); + return updated; + } + + async delete(projectId: string, tabId: string): Promise { + const tab = await this.getById(projectId, tabId); + if (tab.isDefault) { + throw new BadRequestException({ + code: "ERR_TAB_DEFAULT_DELETE", + message: "Varsayılan 'Ana Mimari' sekmesi silinemez.", + }); + } + const def = await this.repo.findDefault(projectId); + if (!def) throw this.tabNotFound("default"); + await this.repo.deleteAndReassign(projectId, tabId, def.id); + } + + async tabGraph(projectId: string, tabId: string): Promise { + const tab = await this.getById(projectId, tabId); + return this.repo.tabGraph(projectId, tab); + } + + async addReference(projectId: string, tabId: string, nodeId: string, x: number, y: number): Promise { + const tab = await this.getById(projectId, tabId); + if (!(await this.repo.nodeExists(projectId, nodeId))) { + throw new NotFoundException({ code: "ERR_NODE_NOT_FOUND", message: `Node '${nodeId}' bulunamadı.` }); + } + // Node'u kendi ev sekmesine referans olarak eklemek anlamsız. + const homeTabId = await this.nodeHomeTab(projectId, nodeId); + if (homeTabId === tab.id) { + throw new BadRequestException({ + code: "ERR_TAB_SELF_REFERENCE", + message: "Node zaten bu sekmenin sahibi; referans eklenemez.", + }); + } + await this.repo.upsertReference(projectId, tab.id, nodeId, x, y); + } + + async removeReference(projectId: string, tabId: string, nodeId: string): Promise { + await this.getById(projectId, tabId); + if (!(await this.repo.removeReference(projectId, tabId, nodeId))) { + throw new NotFoundException({ code: "ERR_REFERENCE_NOT_FOUND", message: `Referans bulunamadı.` }); + } + } + + async saveLayout(projectId: string, tabId: string, items: { nodeId: string; x: number; y: number }[]): Promise { + await this.getById(projectId, tabId); + await this.repo.saveLayout(projectId, tabId, items); + } + + private async nodeHomeTab(projectId: string, nodeId: string): Promise { + return this.repo.nodeHomeTab(projectId, nodeId); + } + + private async assertProject(projectId: string): Promise { + if (!(await this.repo.projectExists(projectId))) { + throw new NotFoundException({ code: "ERR_PROJECT_NOT_FOUND", message: `Project '${projectId}' bulunamadı.` }); + } + } + + private tabNotFound(tabId: string): NotFoundException { + return new NotFoundException({ code: "ERR_TAB_NOT_FOUND", message: `Sekme '${tabId}' bulunamadı.` }); + } +} +``` + +- [ ] **Step 2: `nodeHomeTab` repo metodunu ekle** + +`src/tabs/tabs.repository.ts` içine ekle (sınıfa): +```ts + async nodeHomeTab(projectId: string, nodeId: string): Promise { + const r = await this.neo4j.run( + `MATCH (n:Node {id: $nodeId, projectId: $projectId}) RETURN n.homeTabId AS h`, + { projectId, nodeId }, + ); + return r.records.length ? (r.records[0].get("h") ?? null) : null; + } +``` + +- [ ] **Step 3: Failing test (mock repo)** + +`src/tabs/tabs.service.spec.ts`: +```ts +import { describe, it, expect, vi } from "vitest"; +import { TabsService } from "./tabs.service"; + +function make() { + const repo = { + findDefault: vi.fn(), create: vi.fn(), list: vi.fn(), getById: vi.fn(), + update: vi.fn(), deleteAndReassign: vi.fn(), maxOrder: vi.fn().mockResolvedValue(0), + projectExists: vi.fn().mockResolvedValue(true), nodeExists: vi.fn().mockResolvedValue(true), + upsertReference: vi.fn(), removeReference: vi.fn(), nodeHomeTab: vi.fn(), tabGraph: vi.fn(), + }; + return { svc: new TabsService(repo as any), repo }; +} + +describe("TabsService", () => { + it("ensureDefault varsa oluşturmaz", async () => { + const { svc, repo } = make(); + repo.findDefault.mockResolvedValue({ id: "d", isDefault: true }); + await svc.ensureDefault("p"); + expect(repo.create).not.toHaveBeenCalled(); + }); + + it("ensureDefault yoksa Ana Mimari oluşturur", async () => { + const { svc, repo } = make(); + repo.findDefault.mockResolvedValue(null); + const t = await svc.ensureDefault("p"); + expect(t.name).toBe("Ana Mimari"); + expect(t.isDefault).toBe(true); + expect(repo.create).toHaveBeenCalled(); + }); + + it("default sekme silinemez", async () => { + const { svc, repo } = make(); + repo.getById.mockResolvedValue({ id: "d", isDefault: true }); + await expect(svc.delete("p", "d")).rejects.toThrow(); + }); + + it("node kendi ev sekmesine referans edilemez", async () => { + const { svc, repo } = make(); + repo.getById.mockResolvedValue({ id: "t1", isDefault: false }); + repo.nodeHomeTab.mockResolvedValue("t1"); + await expect(svc.addReference("p", "t1", "n", 0, 0)).rejects.toThrow(); + }); + + it("addReference farklı ev sekmesinde upsert eder", async () => { + const { svc, repo } = make(); + repo.getById.mockResolvedValue({ id: "t2", isDefault: false }); + repo.nodeHomeTab.mockResolvedValue("t1"); + await svc.addReference("p", "t2", "n", 5, 6); + expect(repo.upsertReference).toHaveBeenCalledWith("p", "t2", "n", 5, 6); + }); +}); +``` + +- [ ] **Step 4: Test pass + commit** + +Run: `pnpm vitest run src/tabs/tabs.service.spec.ts` +Expected: PASS (5 test). +```bash +git add src/tabs/tabs.service.ts src/tabs/tabs.repository.ts src/tabs/tabs.service.spec.ts +git commit -m "feat(tabs): service — default koruması, self-ref reddi, home taşıma" +``` + +--- + +### Task 4: TabsController + modül + app.module + +**Files:** +- Create: `src/tabs/tabs.controller.ts`, `src/tabs/tabs.module.ts` +- Modify: `src/app.module.ts` +- Test: `src/tabs/tabs.controller.spec.ts` + +- [ ] **Step 1: Controller** + +`src/tabs/tabs.controller.ts`: + +```ts +import { Body, Controller, Delete, Get, HttpCode, Param, Patch, Post, Put } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiParam, ApiResponse } from "@nestjs/swagger"; +import { TabsService } from "./tabs.service"; +import { CreateTabDto } from "./dto/create-tab.dto"; +import { UpdateTabDto } from "./dto/update-tab.dto"; +import { ReferenceDto } from "./dto/reference.dto"; +import { LayoutDto } from "./dto/layout.dto"; +import { ok } from "../common/envelope"; + +@ApiTags("Tabs (Contexts)") +@Controller("projects/:projectId/tabs") +export class TabsController { + constructor(private readonly service: TabsService) {} + + @Post() + @ApiOperation({ summary: "Sekme oluştur", description: "Yeni context/canvas sekmesi. moduleNodeId opsiyonel (drill-down kaynağı)." }) + @ApiParam({ name: "projectId", description: "Proje UUID" }) + async create(@Param("projectId") projectId: string, @Body() body: CreateTabDto) { + return ok(await this.service.create(projectId, body as any)); + } + + @Get() + @ApiOperation({ summary: "Sekme listesi", description: "order'a göre sıralı." }) + async list(@Param("projectId") projectId: string) { + return ok(await this.service.list(projectId)); + } + + @Get(":tabId") + @ApiOperation({ summary: "Sekme detayı" }) + @ApiResponse({ status: 404, description: "ERR_TAB_NOT_FOUND" }) + async getById(@Param("projectId") projectId: string, @Param("tabId") tabId: string) { + return ok(await this.service.getById(projectId, tabId)); + } + + @Get(":tabId/graph") + @ApiOperation({ summary: "Sekme render içeriği", description: "owned + referenced node'lar (pozisyon + origin) + aralarındaki edge'ler." }) + async graph(@Param("projectId") projectId: string, @Param("tabId") tabId: string) { + return ok(await this.service.tabGraph(projectId, tabId)); + } + + @Patch(":tabId") + @ApiOperation({ summary: "Sekme güncelle (isim/sıra)" }) + async update(@Param("projectId") projectId: string, @Param("tabId") tabId: string, @Body() body: UpdateTabDto) { + return ok(await this.service.update(projectId, tabId, body as any)); + } + + @Delete(":tabId") + @HttpCode(204) + @ApiOperation({ summary: "Sekme sil", description: "Default silinemez. Owned node'lar Ana Mimari'ye taşınır, referanslar kalkar." }) + @ApiResponse({ status: 400, description: "ERR_TAB_DEFAULT_DELETE" }) + async delete(@Param("projectId") projectId: string, @Param("tabId") tabId: string) { + await this.service.delete(projectId, tabId); + } + + @Put(":tabId/references/:nodeId") + @ApiOperation({ summary: "Node'u sekmeye import et / referans konumu güncelle" }) + @ApiResponse({ status: 400, description: "ERR_TAB_SELF_REFERENCE" }) + async addReference( + @Param("projectId") projectId: string, + @Param("tabId") tabId: string, + @Param("nodeId") nodeId: string, + @Body() body: ReferenceDto, + ) { + const { x, y } = body as any; + await this.service.addReference(projectId, tabId, nodeId, x, y); + return ok({ tabId, nodeId, x, y }); + } + + @Delete(":tabId/references/:nodeId") + @HttpCode(204) + @ApiOperation({ summary: "Referansı kaldır (node silinmez)" }) + async removeReference(@Param("projectId") projectId: string, @Param("tabId") tabId: string, @Param("nodeId") nodeId: string) { + await this.service.removeReference(projectId, tabId, nodeId); + } + + @Patch(":tabId/layout") + @ApiOperation({ summary: "Toplu konum kaydet", description: "Sürükleme sonrası: owned → node konumu, referenced → referans konumu." }) + async layout(@Param("projectId") projectId: string, @Param("tabId") tabId: string, @Body() body: LayoutDto) { + const { items } = body as any; + await this.service.saveLayout(projectId, tabId, items); + return ok({ tabId, updated: items.length }); + } +} +``` + +- [ ] **Step 2: Module** + +`src/tabs/tabs.module.ts`: +```ts +import { Module } from "@nestjs/common"; +import { Neo4jModule } from "../neo4j/neo4j.module"; +import { TabsController } from "./tabs.controller"; +import { TabsService } from "./tabs.service"; +import { TabsRepository } from "./tabs.repository"; + +@Module({ + imports: [Neo4jModule], + controllers: [TabsController], + providers: [TabsService, TabsRepository], + exports: [TabsService, TabsRepository], +}) +export class TabsModule {} +``` + +- [ ] **Step 3: app.module.ts'e ekle** + +`src/app.module.ts` import + imports dizisine `TabsModule` ekle. + +- [ ] **Step 4: Controller smoke test** + +`src/tabs/tabs.controller.spec.ts`: +```ts +import { describe, it, expect, vi } from "vitest"; +import { TabsController } from "./tabs.controller"; + +describe("TabsController", () => { + const service = { create: vi.fn().mockResolvedValue({ id: "t" }), addReference: vi.fn(), saveLayout: vi.fn() }; + const c = new TabsController(service as any); + + it("create envelope döner", async () => { + expect(await c.create("p", { name: "X" } as any)).toEqual({ success: true, data: { id: "t" } }); + }); + it("addReference x/y geçirir + envelope", async () => { + const r = await c.addReference("p", "t", "n", { x: 3, y: 4 } as any); + expect(service.addReference).toHaveBeenCalledWith("p", "t", "n", 3, 4); + expect(r).toEqual({ success: true, data: { tabId: "t", nodeId: "n", x: 3, y: 4 } }); + }); +}); +``` + +- [ ] **Step 5: Test + build + commit** + +Run: `pnpm vitest run src/tabs && pnpm build` +Expected: PASS + build temiz. +```bash +git add src/tabs/ src/app.module.ts +git commit -m "feat(tabs): controller + modül (CRUD + references + tab graph + layout)" +``` + +--- + +### Task 5: Node'a homeTabId ekle + +**Files:** +- Modify: `src/nodes/schemas/base.schema.ts`, `src/nodes/dto/create-node.dto.ts`, `src/nodes/nodes.repository.ts`, `src/nodes/nodes.service.ts`, `src/nodes/nodes.module.ts`, `src/projects/projects.repository.ts` +- Test: mevcut `src/nodes/nodes.service.spec.ts` güncelle + +- [ ] **Step 1: BaseNodeSchema + create DTO** + +`src/nodes/schemas/base.schema.ts` — `BaseNodeSchema`'ya ekle: +```ts + homeTabId: z.string().uuid().optional(), +``` +`src/nodes/dto/create-node.dto.ts` — `CreatableBaseFields`'a ekle: +```ts +const CreatableBaseFields = { + projectId: z.string().uuid(), + position: PositionSchema, + homeTabId: z.string().uuid().optional(), // verilmezse default sekme +}; +``` + +- [ ] **Step 2: StoredNode + repository SET/READ** + +`src/nodes/nodes.repository.ts`: +- `StoredNode` interface'ine `homeTabId: string;` ekle. +- `create` Cypher'ına `homeTabId: $homeTabId` ekle + params'a `homeTabId: node.homeTabId`. +- `toStoredNode` (dosya sonundaki): `homeTabId: props.homeTabId,` ekle. + +- [ ] **Step 3: NodesService default tab + NodesModule** + +`src/nodes/nodes.service.ts`: +- Constructor'a `private readonly tabs: TabsService` ekle (import `TabsService`). +- `create` içinde, stored oluşturmadan önce: +```ts +const homeTabId = input.homeTabId ?? (await this.tabs.ensureDefault(urlProjectId)).id; +``` +- `stored`'a `homeTabId` ekle. `CreateInput` tipine `homeTabId?: string` ekle. +- `toNode` ve `Node` dönüşüne homeTabId ekle (BaseNode'da var). + +`src/nodes/nodes.module.ts` — imports'a `TabsModule` ekle (+ import). + +`src/projects/projects.repository.ts` — `nodeFromRecord`'a `homeTabId: props.homeTabId,` ekle (Node tipinde alan opsiyonel). + +> **Not:** `Node`/`BaseNode` tipinde `homeTabId?: string` görünür. `src/nodes/schemas/base.schema.ts` BaseNodeSchema güncellenince tip otomatik gelir. + +- [ ] **Step 4: nodes.service.spec fixture güncelle** + +`src/nodes/nodes.service.spec.ts` — NodesService artık 3. bağımlılık (TabsService) alıyor. Mock ekle: +```ts +const tabs = { ensureDefault: vi.fn().mockResolvedValue({ id: "tab-default" }) }; +// service kurulumunda: new NodesService(repo, projectsRepo, tabs as any) +``` +(Mevcut testlerde NodesService instantiation satırlarına 3. argümanı ekle.) + +- [ ] **Step 5: Test + build + commit** + +Run: `pnpm vitest run src/nodes && pnpm build` +Expected: PASS + build temiz. +```bash +git add src/nodes/ src/projects/projects.repository.ts +git commit -m "feat(nodes): homeTabId — node create default sekmeye yerleşir" +``` + +--- + +### Task 6: Proje oluşturunca default sekme + +**Files:** +- Modify: `src/projects/projects.service.ts`, `src/projects/projects.module.ts` +- Test: `src/projects/projects.service.spec.ts` + +- [ ] **Step 1: ProjectsService.create → ensureDefault** + +`src/projects/projects.service.ts`: +- Constructor'a `private readonly tabs: TabsService` ekle (import). +- `create` içinde `await this.repo.create(stored);` sonrasına: +```ts +await this.tabs.ensureDefault(stored.id); +``` + +`src/projects/projects.module.ts` — imports'a `TabsModule` ekle. + +> **Döngü kontrolü:** TabsModule yalnızca Neo4jModule import eder → Projects→Tabs tek yönlü, döngü yok. + +- [ ] **Step 2: Test güncelle** + +`src/projects/projects.service.spec.ts` — ProjectsService artık TabsService alıyor: +```ts +const tabs = { ensureDefault: vi.fn().mockResolvedValue({ id: "t" }) }; +// new ProjectsService(repo, tabs as any) +``` +Yeni test: +```ts +it("create default sekme oluşturur", async () => { + await service.create({ name: "P", description: "", status: "draft" } as any); + expect(tabs.ensureDefault).toHaveBeenCalled(); +}); +``` + +- [ ] **Step 3: Test + build + commit** + +Run: `pnpm vitest run src/projects && pnpm build` +Expected: PASS. +```bash +git add src/projects/ +git commit -m "feat(projects): proje oluşturunca Ana Mimari sekmesi otomatik kurulur" +``` + +--- + +### Task 7: graph/apply + ai/chat → tabId + +**Files:** +- Modify: `src/graph/dto/apply-graph.dto.ts`, `src/graph/graph.service.ts`, `src/graph/graph.module.ts`, `src/ai/dto/chat.dto.ts`, `src/ai/ai.service.ts` +- Test: mevcut `src/graph/graph.service.spec.ts` güncelle + +- [ ] **Step 1: apply DTO + service tabId** + +`src/graph/dto/apply-graph.dto.ts` — `ApplyGraphSchema`'ya ekle (mutations'ın yanına): +```ts +export const ApplyGraphSchema = z.object({ + tabId: z.string().uuid().optional(), // üretilen node'ların ev sekmesi + mutations: z.object({ /* mevcut */ }).strict(), +}).strict(); +``` + +`src/graph/graph.service.ts`: +- Constructor'a `private readonly tabs: TabsService` ekle (import `TabsService`). +- `apply` başında: `const homeTabId = input.tabId ?? (await this.tabs.ensureDefault(projectId)).id;` +- Node yaratılırken (StoredNode candidate) `homeTabId` ekle: `homeTabId,`. + +`src/graph/graph.module.ts` — imports'a `TabsModule` ekle. + +- [ ] **Step 2: chat DTO + ai.service tabId** + +`src/ai/dto/chat.dto.ts` — `ChatSchema`'ya ekle: +```ts + tabId: z.string().uuid().optional(), +``` +`src/ai/ai.service.ts` — `graphService.apply(projectId, ...)` çağrısına `tabId`'yi geçir (apply input'una `tabId: input.tabId` ekle). AI tool çıktısını apply input'a dönüştüren yerde `tabId` ekle. + +- [ ] **Step 3: graph.service.spec güncelle** + +`src/graph/graph.service.spec.ts` — GraphService artık TabsService alıyor: +```ts +const tabs = { ensureDefault: vi.fn().mockResolvedValue({ id: "tab-default" }) }; +// new GraphService(neo4j, projectsRepo, nodesRepo, rulesEngine, tabs as any) +``` +Apply edilen node'ların `homeTabId` aldığını doğrulayan assertion ekle (stored node'da homeTabId = "tab-default"). + +- [ ] **Step 4: Test + build + commit** + +Run: `pnpm vitest run src/graph src/ai && pnpm build` +Expected: PASS. +```bash +git add src/graph/ src/ai/ +git commit -m "feat(graph,ai): apply + chat opsiyonel tabId — node'lar hedef sekmeye ev sahibi" +``` + +--- + +### Task 8: Migration + constraint + +**Files:** +- Create: `src/neo4j/migrations/data/005-tabs.ts`, `src/neo4j/migrations/002_tab_constraint.cypher` +- Modify: `package.json` + +- [ ] **Step 1: Constraint** + +`src/neo4j/migrations/002_tab_constraint.cypher`: +```cypher +CREATE CONSTRAINT tab_id_unique IF NOT EXISTS FOR (t:Tab) REQUIRE t.id IS UNIQUE; +``` + +- [ ] **Step 2: Migration scripti** + +`src/neo4j/migrations/data/005-tabs.ts`: +```ts +import { randomUUID } from "node:crypto"; +import { Neo4jService } from "../../neo4j.service"; +import { env } from "../../../config/env"; + +/** Her projeye default "Ana Mimari" sekmesi + her node'a homeTabId backfill. + * Idempotent — node.position KORUNUR. */ +async function main(): Promise { + const svc = new Neo4jService({ uri: env.NEO4J_URI, user: env.NEO4J_USER, password: env.NEO4J_PASSWORD }); + await svc.onModuleInit(); + + const projects = await svc.run(`MATCH (p:Project) RETURN p.id AS id`); + let tabs = 0, backfilled = 0; + for (const rec of projects.records) { + const projectId = rec.get("id"); + let def = await svc.run(`MATCH (t:Tab {projectId: $projectId, isDefault: true}) RETURN t.id AS id LIMIT 1`, { projectId }); + let tabId: string; + if (def.records.length === 0) { + tabId = randomUUID(); + const now = new Date().toISOString(); + await svc.run( + `CREATE (t:Tab { id: $id, projectId: $projectId, name: 'Ana Mimari', isDefault: true, order: 0, moduleNodeId: null, createdAt: datetime($now), updatedAt: datetime($now) })`, + { id: tabId, projectId, now }, + ); + tabs++; + } else { + tabId = def.records[0].get("id"); + } + const res = await svc.run( + `MATCH (n:Node {projectId: $projectId}) WHERE n.homeTabId IS NULL SET n.homeTabId = $tabId RETURN count(n) AS c`, + { projectId, tabId }, + ); + backfilled += Number(res.records[0].get("c")); + } + + await svc.onModuleDestroy(); + console.log(`✓ Tabs migration: ${tabs} default sekme, ${backfilled} node homeTabId backfill.`); +} + +main().catch((e) => { console.error("✗ Tabs migration failed:", e); process.exit(1); }); +``` + +- [ ] **Step 3: package.json script + çalıştır** + +`scripts`'e ekle: +```json +"migrate:tabs": "tsx --env-file=.env src/neo4j/migrations/data/005-tabs.ts" +``` +Run: `pnpm neo4j:migrate && pnpm migrate:tabs` +Expected: constraint kurulur + `✓ Tabs migration: N default sekme, M node homeTabId backfill.` (ikinci çalıştırmada 0/0 — idempotent) + +- [ ] **Step 4: Commit** + +```bash +git add src/neo4j/migrations/ package.json +git commit -m "feat(neo4j): tabs migration — default sekme + homeTabId backfill + tab_id_unique" +``` + +--- + +### Task 9: E2E + tam test + push + +**Files:** +- Create: `test/tabs.e2e-spec.ts` +- Referans: `test/patterns.e2e-spec.ts` (bootstrap) + +- [ ] **Step 1: E2E** + +`test/tabs.e2e-spec.ts` — `test/patterns.e2e-spec.ts`'in bootstrap'ını birebir izle (Testcontainers Neo4j 5-community + overrideProvider(Neo4jService) + global ZodPipe + prefix + 4 filter). EMBEDDINGS override GEREKMEZ (tabs embedding kullanmaz). Constraint'leri beforeAll'da kur: +```ts +await neo4j.run("CREATE CONSTRAINT node_id_unique IF NOT EXISTS FOR (n:Node) REQUIRE n.id IS UNIQUE"); +await neo4j.run("CREATE CONSTRAINT tab_id_unique IF NOT EXISTS FOR (t:Tab) REQUIRE t.id IS UNIQUE"); +``` +Bir proje + bir node oluştur (API üzerinden), sonra: + +```ts +const base = "/api/v1"; +let projectId: string; +let nodeId: string; + +it("proje açılınca Ana Mimari sekmesi oluşur", async () => { + const p = await request(app.getHttpServer()).post(`${base}/projects`).send({ name: "Tab E2E" }).expect(201); + projectId = p.body.data.id; + const tabs = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs`).expect(200); + expect(tabs.body.data.length).toBe(1); + expect(tabs.body.data[0].isDefault).toBe(true); + expect(tabs.body.data[0].name).toBe("Ana Mimari"); +}); + +it("node default sekmeye ev sahibi olur, tab graph'ta görünür", async () => { + const n = await request(app.getHttpServer()).post(`${base}/projects/${projectId}/nodes`).send({ + projectId, position: { x: 10, y: 20 }, type: "Service", + properties: { ServiceName: "OrderSvc", Description: "d", IsTransactionScoped: false, Methods: [{ MethodName: "x", ReturnType: "void" }] }, + }).expect(201); + nodeId = n.body.data.id; + const tabs = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs`).expect(200); + const defId = tabs.body.data[0].id; + const g = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs/${defId}/graph`).expect(200); + expect(g.body.data.nodes).toHaveLength(1); + expect(g.body.data.nodes[0].isReference).toBe(false); + expect(g.body.data.nodes[0].position).toEqual({ x: 10, y: 20 }); +}); + +it("yeni sekme + node import (referans) round-trip", async () => { + const t = await request(app.getHttpServer()).post(`${base}/projects/${projectId}/tabs`).send({ name: "Sipariş" }).expect(201); + const tabId = t.body.data.id; + // import et + await request(app.getHttpServer()).put(`${base}/projects/${projectId}/tabs/${tabId}/references/${nodeId}`).send({ x: 99, y: 88 }).expect(200); + const g = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs/${tabId}/graph`).expect(200); + expect(g.body.data.nodes).toHaveLength(1); + expect(g.body.data.nodes[0].isReference).toBe(true); + expect(g.body.data.nodes[0].position).toEqual({ x: 99, y: 88 }); + // referansı kaldır + await request(app.getHttpServer()).delete(`${base}/projects/${projectId}/tabs/${tabId}/references/${nodeId}`).expect(204); + const g2 = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs/${tabId}/graph`).expect(200); + expect(g2.body.data.nodes).toHaveLength(0); +}); + +it("default sekme silinemez (400)", async () => { + const tabs = await request(app.getHttpServer()).get(`${base}/projects/${projectId}/tabs`).expect(200); + const defId = tabs.body.data.find((t: any) => t.isDefault).id; + await request(app.getHttpServer()).delete(`${base}/projects/${projectId}/tabs/${defId}`).expect(400); +}); + +it("sekme silinince owned node Ana Mimari'ye taşınır", async () => { + const t = await request(app.getHttpServer()).post(`${base}/projects/${projectId}/tabs`).send({ name: "Geçici" }).expect(201); + const tabId = t.body.data.id; + const n = await request(app.getHttpServer()).post(`${base}/projects/${projectId}/nodes`).send({ + projectId, position: { x: 1, y: 1 }, homeTabId: tabId, type: "Cache", + properties: { CacheName: "C", Description: "d", KeyPattern: "k", TTL_Seconds: 60, Engine: "Redis" }, + }).expect(201); + await request(app.getHttpServer()).delete(`${base}/projects/${projectId}/tabs/${tabId}`).expect(204); + // node hâlâ var (mantıksal grafta) + await request(app.getHttpServer()).get(`${base}/projects/${projectId}/nodes/${n.body.data.id}`).expect(200); +}); +``` + +- [ ] **Step 2: Tüm test paketi** + +Run: `pnpm test` +Expected: tüm unit PASS. + +Run: `pnpm test:e2e` +Expected: tabs + nodes + patterns e2e hepsi PASS. + +- [ ] **Step 3: Canlı duman testi (opsiyonel ama önerilir)** + +```bash +pnpm build && pnpm neo4j:migrate && pnpm migrate:tabs +fuser -k 4000/tcp 2>/dev/null; set -a; source .env; set +a; PORT=4000 node dist/main.js & +# proje oluştur → GET tabs (Ana Mimari görünmeli) → node ekle → tab graph +``` +Expected: yeni proje Ana Mimari sekmesiyle gelir, node tab graph'ta owned olarak görünür. + +- [ ] **Step 4: Memory + push** + +`project_solarch_backend_phase1.md`'ye Tabs özeti ekle (model + endpoint'ler + migration + gotchas). +```bash +git add test/tabs.e2e-spec.ts +git commit -m "test(tabs): e2e — default sekme + node import (referans) + home taşıma" +git push origin main +``` + +--- + +## Faz Çıkış Kriterleri (Spec ile) + +- [ ] Tek ev + referans modeli: node.homeTabId + (:Tab)-[:REFERENCES]->(:Node) (Task 1,2,5) +- [ ] Tabs CRUD + references + tab graph + layout (Task 2,3,4) +- [ ] Default sekme koruması + home taşıma + self-ref reddi (Task 3) +- [ ] Proje oluşturunca + node create default sekme (Task 5,6) +- [ ] apply/ai opsiyonel tabId (Task 7) +- [ ] Migration default sekme + homeTabId backfill, idempotent, node.position korunur (Task 8) +- [ ] E2E round-trip (Task 9) + +## Notlar + +- **node.position KORUNUR** — bu faz büyük ölçüde additive; apply/AI/CRUD/graph mevcut akışları çalışmaya devam eder. +- **Döngü önleme:** TabsModule yalnızca Neo4jModule import eder; existence check'leri kendi Cypher'ıyla. Projects/Nodes/Graph → Tabs tek yönlü. +- View sekmeleri (Infra/DBSchema/Flow) ve semantik-zoom out of scope (sonraki faz). +- Migration sırası: önce `neo4j:migrate` (constraint .cypher), sonra `migrate:tabs` (data). diff --git a/apps/server/docs/specs/2026-05-21-node-types-design.md b/apps/server/docs/specs/2026-05-21-node-types-design.md new file mode 100644 index 0000000..7ab47cd --- /dev/null +++ b/apps/server/docs/specs/2026-05-21-node-types-design.md @@ -0,0 +1,614 @@ +# Solarch Backend — Node Types & API (Phase 1) Tasarımı + +**Tarih:** 2026-05-21 +**Durum:** Brainstorm tamamlandı, kullanıcı onayı alındı +**Repo:** `solarch_backend` (ayrı git repo — `Solarch/` core'dan bağımsız) +**Önceki MVP:** `Solarch/web/` (geride bırakılıyor, referans değil) +**Tek doğruluk kaynağı:** `Solarch/plans/` dokümanları + +## 1. Vizyon ve Phase 1 Kapsamı + +### Genel vizyon +Solarch, doğal dil / sketch ile çizilen mimariyi Rules Engine + AI ile katı şema standartlarına oturtup deterministik scaffold + cerrahi AI ile NestJS/FastAPI kod üreten bir "architecture-to-code" platformu. Backend'in nihai görevleri: node/edge persistence, Rules Engine, AI batch apply (transaction + suggestion loop), kod üretim motoru, GraphRAG. + +### Phase 1 kapsamı (bu spec) +**Sadece Node CRUD + şema doğrulama.** Aşağıdaki katmanlar Phase 1'de yok: + +- Edge işlemleri (Phase 2) +- Rules Engine (whitelist/blacklist/koşullu — Phase 2) +- AI batch apply (`/graph/apply`) (Phase 3) +- LangGraph agent (Phase 3) +- Vector / GraphRAG (Phase 4) +- Kod üretim motoru (Phase 5) +- Auth, rate limit, multi-tenancy (sonraki) + +### Phase 1 node taksonomisi +Sadece **Veri ailesi** (5 tip): `Table`, `DTO`, `Model`, `Enum`, `View`. + +## 2. Mimari Kararlar (Stack) + +| Karar | Seçim | Gerekçe | +|---|---|---| +| Dil | TypeScript | Tek-dil ekosistemi tercihi | +| Framework | NestJS | Modüler yapı + DI + decorator disiplini | +| Validation | Zod | Discriminated union + JSON Schema export + tip çıkarımı | +| DB | Neo4j 5 (community) | Plans/Veritabanı Stratejisi nihai hedefi — graph traversal Phase 2+ Rules Engine için bedava | +| DB driver | `neo4j-driver` (resmi, raw Cypher) | Plans/Fraktal Graf Backend Mantığı Cypher pattern'leri veriyor; OGM soyutlaması Phase 1'de overhead | +| Paket yöneticisi | pnpm | Modern, hızlı | +| Test runner | Vitest (NestJS Jest yerine) | Hızlı, modern, ESM-friendly | +| E2E DB | Testcontainers | Geçici Neo4j container | +| Container | Docker Compose | Lokal Neo4j | + +## 3. Repo Organizasyonu + +`solarch_backend` ayrı bir git repo. Solarch core repo'sundan bağımsız: + +``` +solarch_backend/ +├── src/ +│ ├── main.ts bootstrap (CORS, global pipes/filters) +│ ├── app.module.ts root module +│ ├── config/ +│ │ ├── env.ts Zod ile env validation (fail-fast) +│ │ └── neo4j.config.ts URI/user/pass +│ ├── neo4j/ +│ │ ├── neo4j.module.ts @Global() module, driver singleton +│ │ ├── neo4j.service.ts session/transaction wrapper +│ │ ├── migrations/ +│ │ │ └── 001_constraints.cypher id unique + project index +│ │ └── cypher.ts küçük query builder helpers +│ ├── nodes/ +│ │ ├── nodes.module.ts +│ │ ├── nodes.controller.ts REST endpoint'leri +│ │ ├── nodes.service.ts business logic +│ │ ├── nodes.repository.ts Cypher sorgular +│ │ ├── schemas/ +│ │ │ ├── base.schema.ts BaseNodeSchema (DEĞİŞMEZ KURAL) +│ │ │ ├── table.schema.ts +│ │ │ ├── dto.schema.ts +│ │ │ ├── model.schema.ts +│ │ │ ├── enum.schema.ts +│ │ │ ├── view.schema.ts (PLACEHOLDER — plans'ta detay yok) +│ │ │ └── index.ts NodeSchema = discriminatedUnion +│ │ └── dto/ +│ │ ├── create-node.dto.ts +│ │ ├── update-node.dto.ts +│ │ └── node-response.dto.ts +│ └── common/ +│ ├── pipes/zod-validation.pipe.ts +│ ├── filters/ +│ │ ├── schema-error.filter.ts ZodError → ERR_SCHEMA_INVALID envelope +│ │ ├── not-found.filter.ts → 404 envelope +│ │ └── conflict.filter.ts → 409 envelope +│ └── envelope.ts { success, data | error } helper +├── test/ +│ └── nodes.e2e-spec.ts Testcontainers + Neo4j round-trip +├── docker-compose.yml Neo4j 5 + APOC +├── .env.example +├── .gitignore +├── nest-cli.json +├── tsconfig.json +├── tsconfig.build.json +├── package.json +├── pnpm-lock.yaml +├── vitest.config.ts +└── README.md +``` + +## 4. Base Node Schema (DEĞİŞMEZ KURAL) + +Her yeni node tipi bu base'i miras alır. Yeni tip ekleyen geliştirici **sadece** `type` literal'ı ve `properties` shape'ini tanımlar. + +```ts +// src/nodes/schemas/base.schema.ts +import { z } from "zod"; + +export const PositionSchema = z.object({ + x: z.number(), + y: z.number(), +}); + +export const BaseNodeSchema = z.object({ + id: z.string().uuid(), + projectId: z.string().uuid(), + position: PositionSchema, + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); +``` + +**Kural:** Bu beş alan **request payload'da gelir** (kullanıcı kararı): +- `id`, `createdAt`, `updatedAt` **opsiyonel** — verilirse aynen kabul edilir (AI batch / import senaryolarında deterministik replay), yoksa server üretir. +- `projectId`, `position` **zorunlu** — request'te mutlaka olmalı. + +## 5. Veri Ailesi Node Şemaları + +Hepsi `Solarch/plans/Solarch Node Şemaları (Node Schemas).md` "1. Veri ve Şema Katmanı" bölümünden birebir alınmış. View hariç (plans'ta detay yok — placeholder). + +### 5.1 Table + +```ts +export const TableNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Table"), + properties: z.object({ + TableName: z.string().min(1), + Description: z.string().min(1), + Columns: z.array(z.object({ + Name: z.string().min(1), + DataType: z.enum(["INT", "VARCHAR", "TEXT", "BOOLEAN", "DATETIME", "UUID", "FLOAT", "JSON"]), + Length: z.number().int().positive().optional(), + IsPrimaryKey: z.boolean(), + IsForeignKey: z.boolean(), + References: z.string().optional(), + IsNotNull: z.boolean(), + IsUnique: z.boolean(), + AutoIncrement: z.boolean(), + DefaultValue: z.string().optional(), + })).min(1), + Indexes: z.array(z.object({ + IndexName: z.string().min(1), + Columns: z.array(z.string()).min(1), + Type: z.enum(["B-Tree", "Hash"]), + })).default([]), + }).strict(), +}).strict(); +``` + +### 5.2 DTO + +```ts +export const DTONodeSchema = BaseNodeSchema.extend({ + type: z.literal("DTO"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + Fields: z.array(z.object({ + Name: z.string().min(1), + DataType: z.string().min(1), + IsRequired: z.boolean(), + ValidationRule: z.string().optional(), + IsArray: z.boolean(), + })).min(1), + }).strict(), +}).strict(); +``` + +### 5.3 Model / Entity + +```ts +export const ModelNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Model"), + properties: z.object({ + ClassName: z.string().min(1), + Description: z.string().min(1), + Properties: z.array(z.object({ + Name: z.string().min(1), + Type: z.string().min(1), + })).min(1), + Methods: z.array(z.object({ + MethodName: z.string().min(1), + ReturnType: z.string().min(1), + })).default([]), + }).strict(), +}).strict(); +``` + +### 5.4 Enum + +```ts +export const EnumNodeSchema = BaseNodeSchema.extend({ + type: z.literal("Enum"), + properties: z.object({ + Name: z.string().min(1), + Description: z.string().min(1), + Values: z.array(z.string().min(1)).min(1), + }).strict(), +}).strict(); +``` + +### 5.5 View (PLACEHOLDER — plans'ta detay yok) + +```ts +export const ViewNodeSchema = BaseNodeSchema.extend({ + type: z.literal("View"), + properties: z.object({ + ViewName: z.string().min(1), + Description: z.string().min(1), + Definition: z.string().min(1), // SQL/aggregate metni + SourceTables: z.array(z.string()).min(1), + Materialized: z.boolean(), + }).strict(), +}).strict(); +``` + +Plans güncellendiğinde bu şema üzerine yazılır. + +### 5.6 Description disiplini + +`Description` alanı **tüm 5 tipte zorunlu** (kullanıcı kararı). Plans'ta yalnız Table'da explicit listeli ama disiplin için tüme uygulanır. + +### 5.7 Discriminated Union + +```ts +// src/nodes/schemas/index.ts +export const NodeSchema = z.discriminatedUnion("type", [ + TableNodeSchema, + DTONodeSchema, + ModelNodeSchema, + EnumNodeSchema, + ViewNodeSchema, +]); + +export type Node = z.infer; +export type NodeKind = Node["type"]; + +export const KIND_LABELS: Record = { + Table: "Table", + DTO: "DTO", + Model: "Model", + Enum: "Enum", + View: "View", +}; +``` + +## 6. "Yeni Node Tipi Ekleme" Kuralı (Codified) + +Yeni bir kind eklemek için **üç şart**: + +1. **`schemas/.schema.ts`** dosyası oluştur → `BaseNodeSchema.extend({ type: z.literal("Foo"), properties: z.object({...}).strict() }).strict()`. +2. **`schemas/index.ts`**'teki `NodeSchema` discriminated union'una eklenir + `KIND_LABELS` haritası güncellenir. +3. **Test:** `schemas/.schema.spec.ts` dosyasında valid + invalid payload örnekleri. + +Bu disiplinin sonuçları: +- Union'a girmeyen tip controller'a **compile-time'da** giremez (TS). +- ValidationPipe runtime'da reddeder (Zod). +- Tüm node'lar zorunlu base alanlara sahiptir (id, projectId, position, timestamps). + +## 7. API Kontratları + +### 7.1 Endpoint yüzeyi + +| Method | Path | Amaç | +|---|---|---| +| POST | `/api/v1/projects/:projectId/nodes` | Yeni node oluştur | +| GET | `/api/v1/projects/:projectId/nodes` | Listele (opsiyonel `?type=Table` filter) | +| GET | `/api/v1/projects/:projectId/nodes/:nodeId` | Tekil node | +| PATCH | `/api/v1/projects/:projectId/nodes/:nodeId` | Kısmi güncelleme | +| DELETE | `/api/v1/projects/:projectId/nodes/:nodeId` | Sil (idempotent) | + +Plans/API Spesifikasyonları 1.1 sadece POST tanımlıyor; CRUD'u tamamlamak için Phase 1'e ek (plans'ı **genişletiyoruz, çelişmiyoruz**). + +### 7.2 Request envelope (POST) + +```jsonc +// POST /api/v1/projects/prj_xxx/nodes +{ + "id": "nd_8f7a9c2b", // opsiyonel + "type": "Table", + "projectId": "prj_xxx", // URL ile match zorunlu + "position": { "x": 150, "y": 300 }, + "createdAt": "2026-05-21T10:30:00Z", // opsiyonel + "updatedAt": "2026-05-21T10:30:00Z", // opsiyonel + "properties": { + "TableName": "users", + "Description": "User entities", + "Columns": [...], + "Indexes": [] + } +} +``` + +### 7.3 Response envelope (her endpoint için tutarlı) + +**Başarı:** +```jsonc +{ "success": true, "data": { /* tam node objesi */ } } +``` + +**Hata:** +```jsonc +{ + "success": false, + "error": { + "code": "ERR_SCHEMA_INVALID", + "message": "Gönderilen özellikler 'Table' şeması ile uyuşmuyor.", + "details": [{ "field": "properties.Columns[0].DataType", "issue": "..." }] + } +} +``` + +`details` alanı sadece `ERR_SCHEMA_INVALID`'de var. Plans/API Spec birebir. + +### 7.4 HTTP status → error code haritası + +| Status | Code | Senaryo | +|---|---|---| +| 201 | — | POST başarılı | +| 200 | — | GET / PATCH başarılı | +| 204 | — | DELETE başarılı (idempotent) | +| 400 | `ERR_SCHEMA_INVALID` | Zod validation fail (plans 1.1) | +| 400 | `ERR_PROJECT_MISMATCH` | URL projectId ≠ body projectId | +| 400 | `ERR_KIND_IMMUTABLE` | PATCH `type` değiştirmeye çalıştı | +| 404 | `ERR_NODE_NOT_FOUND` | GET/PATCH/DELETE'de node yok | +| 409 | `ERR_ID_CONFLICT` | Client id verdi, zaten kullanılıyor | +| 409 | `ERR_NAME_DUPLICATE` | `*Name` proje içinde unique constraint ihlali | +| 500 | `ERR_INTERNAL` | Beklenmeyen — DB bağlantısı vs. | + +### 7.5 PATCH semantiği — field-level replace + +PATCH body'sinde top-level alanlar **opsiyonel**: + +```jsonc +PATCH /api/v1/projects/prj_xxx/nodes/nd_8f7a9c2b +{ + "position": { "x": 200, "y": 400 } // sadece position update — drag senaryosu +} +``` + +veya + +```jsonc +{ + "properties": { /* tam yeni properties */ } // properties tamamen replace +} +``` + +**Kurallar:** +- Verilen field tam objesiyle **replace** edilir (deep merge yok). +- Verilmeyen field dokunulmaz. +- `type` immutable — değiştirmeye çalışırsa 400 `ERR_KIND_IMMUTABLE`. +- `id`, `projectId`, `createdAt` immutable. +- `updatedAt` server tarafından otomatik güncellenir. + +Bu **JSON Merge Patch**'in basitleştirilmiş hali. Phase 1'de en az bug, en öngörülebilir. + +### 7.6 Cross-cutting + +- **Auth:** Phase 1'de yok (lokal geliştirme). +- **CORS:** `CORS_ORIGIN` env'iyle whitelist (varsayılan `http://localhost:3000`). +- **Content-Type:** `application/json` zorunlu. +- **Rate limit:** Phase 1'de yok. +- **Pagination:** Phase 1'de yok (tüm node'lar dönülür); ileride `?limit&offset` eklenir. + +## 8. Persistence — Neo4j Veri Modeli + +### 8.1 Node yapısı + +Her node iki label taşır: `:Node` (global) + `:` (kind-spesifik). + +```cypher +CREATE (n:Node:Table { + id: "nd_8f7a9c2b", + projectId: "prj_xxx", + positionX: 150.0, + positionY: 300.0, + createdAt: datetime("2026-05-21T10:30:00Z"), + updatedAt: datetime("2026-05-21T10:30:00Z"), + properties: "{\"TableName\":\"users\",...}" // JSON string +}) +``` + +**Kararlar:** +- `properties` JSON string olarak tutulur. Gerekçe: Neo4j map type'ı array-of-object'i destekler ama indekslenemez ve `Columns[].IsPrimaryKey` gibi nested predicate query'leri Cypher'da çirkin. Phase 1'de properties üzerinden query yok (sadece read/write). Phase 2'de struct'a flatten ederiz. +- `position` x/y ayrı kolonlar — drag sırasında sadece bunlar update edilebilir. +- `:Node` ortak label sayesinde `id` global unique constraint tek tanımla halledilir. + +### 8.2 Constraint'ler ve indeksler (bootstrap migration) + +```cypher +-- 001_constraints.cypher +CREATE CONSTRAINT node_id_unique IF NOT EXISTS + FOR (n:Node) REQUIRE n.id IS UNIQUE; + +CREATE INDEX node_project_idx IF NOT EXISTS + FOR (n:Node) ON (n.projectId); +``` + +Kind bazlı filtreler için ek index gereksiz — kind zaten label olarak duruyor (`:Table`, `:DTO` vs.), Neo4j label match'i index-equivalent hızdadır. + +### 8.3 Temel Cypher sorguları + +**Create:** +```cypher +CREATE (n:Node:Table {id: $id, projectId: $projectId, positionX: $x, positionY: $y, + createdAt: datetime($createdAt), updatedAt: datetime($updatedAt), + properties: $properties}) +RETURN n; +``` + +**Get by id:** +```cypher +MATCH (n:Node {id: $id, projectId: $projectId}) +RETURN n, labels(n) AS labels; +``` + +**List by project (+ optional kind):** +```cypher +MATCH (n:Node {projectId: $projectId}) +WHERE $kind IS NULL OR $kind IN labels(n) +RETURN n, labels(n) AS labels; +``` + +**Patch (field-level):** +```cypher +MATCH (n:Node {id: $id, projectId: $projectId}) +SET n += $partial +RETURN n, labels(n) AS labels; +``` + +`$partial` Service tarafında hazırlanır: +- `body.position` verilirse → `{ positionX, positionY }` flatten +- `body.properties` verilirse → `{ properties: JSON.stringify(props) }` +- Her durumda `updatedAt: datetime(now)` eklenir + +**Delete:** +```cypher +MATCH (n:Node {id: $id, projectId: $projectId}) +DELETE n; +``` + +### 8.4 Unique-name kontrolü + +`*Name` (TableName/Name/ClassName/ViewName) proje içinde unique. Phase 1'de **uygulama katmanında** kontrol edilir (`properties` JSON string olduğu için DB-level constraint yok). Service `create`/`patch`'te check + 409 `ERR_NAME_DUPLICATE`. Phase 2'de properties struct'a açıldığında DB constraint'e yükseltilir. + +## 9. Validation Pipeline + +``` +Request body + → ZodValidationPipe (parse via NodeSchema) + → kind discriminator otomatik + → .strict() bilinmeyen alanları reddeder + → Controller method (typed payload) + → Service (business logic) + → Repository (Cypher exec) + → Neo4j +``` + +### 9.1 ZodValidationPipe + +```ts +@Injectable() +export class ZodValidationPipe implements PipeTransform { + constructor(private schema: ZodSchema) {} + transform(value: unknown) { + return this.schema.parse(value); // throws ZodError on fail + } +} +``` + +### 9.2 SchemaErrorFilter + +`@Catch(ZodError)` global filter: + +```ts +toEnvelope(err: ZodError) { + return { + success: false, + error: { + code: "ERR_SCHEMA_INVALID", + message: "Gönderilen özellikler şema ile uyuşmuyor.", + details: err.issues.map(i => ({ + field: i.path.join("."), + issue: i.message, + })), + }, + }; +} +``` + +Diğer filter'lar: `NotFoundExceptionFilter` → 404, `ConflictExceptionFilter` → 409, `InternalErrorFilter` → 500. + +## 10. Configuration + +### 10.1 `.env.example` + +```bash +NODE_ENV=development +PORT=4000 + +NEO4J_URI=bolt://localhost:7687 +NEO4J_USER=neo4j +NEO4J_PASSWORD=solarch_dev_password + +CORS_ORIGIN=http://localhost:3000 +``` + +### 10.2 `config/env.ts` — boot-time validation + +```ts +const EnvSchema = z.object({ + NODE_ENV: z.enum(["development", "production", "test"]).default("development"), + PORT: z.coerce.number().int().positive().default(4000), + NEO4J_URI: z.string().url(), + NEO4J_USER: z.string().min(1), + NEO4J_PASSWORD: z.string().min(1), + CORS_ORIGIN: z.string().default("http://localhost:3000"), +}); + +export const env = EnvSchema.parse(process.env); +``` + +Eksik env varsa server up olmaz (fail-fast). + +### 10.3 `docker-compose.yml` + +```yaml +services: + neo4j: + image: neo4j:5-community + container_name: solarch-neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + NEO4J_AUTH: neo4j/solarch_dev_password + NEO4J_PLUGINS: '["apoc"]' + volumes: + - solarch_neo4j_data:/data + restart: unless-stopped + +volumes: + solarch_neo4j_data: +``` + +## 11. Test Stratejisi + +### 11.1 Unit testler + +- `src/nodes/schemas/*.spec.ts` — her kind için **valid payload** + **invalid payload** (zorunlu alan eksik, bilinmeyen alan, yanlış tip). +- `src/common/pipes/zod-validation.pipe.spec.ts` — ZodError → fırlatılır. +- `src/common/filters/schema-error.filter.spec.ts` — envelope formatı. + +### 11.2 E2E testler + +`test/nodes.e2e-spec.ts`: +- Testcontainers ile geçici Neo4j container. +- Her 5 kind için tam CRUD round-trip (POST → GET → PATCH → DELETE). +- Error path'leri: ERR_SCHEMA_INVALID, ERR_NODE_NOT_FOUND, ERR_NAME_DUPLICATE, ERR_KIND_IMMUTABLE. + +### 11.3 Komutlar + +```jsonc +// package.json scripts +{ + "dev": "nest start --watch", + "build": "nest build", + "start": "node dist/main.js", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "vitest run --config vitest.e2e.config.ts", + "lint": "eslint src --ext .ts", + "neo4j:up": "docker compose up -d", + "neo4j:down": "docker compose down", + "neo4j:migrate": "tsx src/neo4j/migrations/run.ts" +} +``` + +## 12. Phase 1 Çıkış Kriterleri + +1. `solarch_backend` repo'su initialize edilmiş (git, package.json, tsconfig). +2. NestJS app boot ediyor, `/api/v1/health` endpoint'i 200 dönüyor. +3. Docker Compose ile Neo4j ayağa kalkıyor. +4. Migration script `node_id_unique` constraint'ini ve `node_project_idx` indeksini kuruyor. +5. 5 Veri ailesi node tipi için (Table, DTO, Model, Enum, View) tam CRUD endpoint'leri çalışıyor. +6. Tüm endpoint'ler plans/API Spec'indeki response envelope formatına uyuyor. +7. Unit + E2E testler geçiyor (5 kind × CRUD + error path'leri). +8. README implementasyon notları + `.env.example` + `docker-compose.yml` mevcut. + +## 13. Out of Scope (Phase 2+) + +Aşağıdakiler bu spec'in kapsamında **değil**: + +- **Phase 2**: Edge schemas + Edge CRUD + `/edges/validate` + Rules Engine (whitelist/blacklist). +- **Phase 2.5**: Conditional rules (döngüsel bağımlılık, encapsulation, tip uyumsuzluğu). +- **Phase 3**: AI batch apply (`/graph/apply`) + LangGraph agent loop + suggestion-driven self-correction. +- **Phase 4**: Vector DB + GraphRAG + "Chat with Architecture". +- **Phase 5**: Kod üretim motoru (AST scaffold + cerrahi AI + `.cursorrules` export). +- **Sonra**: Auth, multi-tenancy, rate limit, pagination, audit log. +- **Diğer node aileleri**: İş Mantığı (Service/Worker/EventHandler/Orchestrator), Erişim (Controller/APIGateway/MessageQueue), Altyapı (Repository/Cache/ExternalService), İstemci (FrontendApp/MobileApp/UIComponent), Güvenlik (Middleware/Exception/Auth), Yapı (Module/BoundedContext) — Phase 1.5 olarak şablon hazır olduktan sonra eklenir. + +## 14. Açık Notlar + +- **View placeholder**: Plans'ta detay yok. Phase 1 placeholder şeması (ViewName/Description/Definition/SourceTables/Materialized) plans güncellenince üzerine yazılır. +- **Solarch core repo ile entegrasyon**: `solarch_backend` ayrı repo. Solarch core (`web/`) sonradan bu backend'i `CORS_ORIGIN` aracılığıyla çağırabilir. Phase 1'de iki repo arası shared type paketi **yok**; her iki taraf kendi Zod şemalarını taşır. Phase 2'de `@solarch/contracts` adında bir npm paketi (veya monorepo'ya geçiş) değerlendirilir. +- **Mevcut `Solarch/web/migrate-to-mongo.ts`**: `solarch_backend` Neo4j'e geçtiği için bu migration kullanım dışı kalır. `web/` MVP olarak hayatta kalmaya devam edebilir ama yeni özellik almaz. diff --git a/apps/server/docs/specs/2026-05-22-node-enrichment-design.md b/apps/server/docs/specs/2026-05-22-node-enrichment-design.md new file mode 100644 index 0000000..494910d --- /dev/null +++ b/apps/server/docs/specs/2026-05-22-node-enrichment-design.md @@ -0,0 +1,319 @@ +# Node Properties Enrichment — Tasarım (Codegen-Ready Derinlik) + +**Tarih:** 2026-05-22 +**Durum:** Brainstorm tamamlandı, kullanıcı onayı alındı +**Repo:** solarch-backend +**Önceki:** Phase 1-3B tamamlandı (21 node tipi + 16 edge + Rules + Batch + AI agent) + +## 1. Amaç + +21 node tipinin `properties` şemalarını **codegen-ready** derinliğe çıkarmak. Tek hedef üç ihtiyacı birden karşılar: + +- **Codegen (Phase 5):** Bu şemalardan gerçek kod üretilebilir — DB DDL (CREATE TABLE + constraints + indexes), ORM entity, DTO class, controller scaffold. +- **AI üretimi:** AI agent daha gerçekçi/eksiksiz mimari üretir (zengin ama net alanlar). +- **UI inspector:** Frontend node düzenleme paneli her alanı tip/badge/hint ile gösterir. + +Codegen en yüksek bar olduğu için onu karşılayan şema AI + UI'ı da kapsar. + +## 2. Genel Yaklaşım (Kararlar) + +| Karar | İçerik | +|---|---| +| **Derinlik** | Tam DB/kod modeli (composite key, FK actions, check constraints, method signatures, validation rules) | +| **Zorunluluk** | Yeni alanlar **required** (production-grade; yarım node yok). Mevcut DB node'ları migration ile doldurulur. | +| **Cross-reference** | Property'lerde **isim string referansı** (örn kolon `EnumRef: "OrderStatus"`). Node id değil. Edge'lerden bağımsız — codegen resolve eder. Edge'ler ayrı kalır (görsel + Rules Engine). | +| **Migration** | `GRAPH_SCHEMA_VERSION` eklenir + her faz bump. tsx migration script: mevcut node `properties` JSON'larını parse → yeni zorunlu alanları default ile doldur → re-serialize. | +| **UI metadata** | Zod `.describe()` her alanda → `zodV3ToOpenAPI` ile JSON Schema'da görünür. `node-types/:id` type/required/enum/description döner. Badge (PK/FK vb.) için `node-types/registry.ts`'te `fieldHints`. | +| **AI prompt** | Her faz sonunda `src/ai/prompts/system-prompt.ts` node şema rehberi güncellenir (yoksa AI eski şema üretip `ERR_SCHEMA_INVALID` alır). | +| **Strateji** | Spec tüm 21 node'u kapsar. Implement fazlı: **Faz A** Veri ailesi → **Faz B** İş/Erişim → **Faz C** Altyapı/İstemci/Güvenlik/Konfig/Yapı. | + +### Cross-reference enum'ları +Tüm `*Ref` alanları o referansın hedef node `*Name`'ine eşittir (proje içi benzersiz). Codegen ve gelecekteki bir `resolveRefs` katmanı bunları gerçek node id'lerine çözer. Phase 1.5'te referans **doğrulaması yapılmaz** (esneklik); Phase 2 (Rules) sonrası opsiyonel ref-validation eklenebilir. + +## 3. Faz A — Veri Ailesi (codegen çekirdeği) + +### Table +```ts +const ColumnSchema = z.object({ + Name: z.string().min(1), + DataType: z.enum(["INT","BIGINT","VARCHAR","TEXT","BOOLEAN","DATETIME","DATE","UUID","FLOAT","DECIMAL","JSON","ENUM"]), + Length: z.number().int().positive().optional(), // VARCHAR(n) + Precision: z.number().int().positive().optional(), // DECIMAL(p,s) + Scale: z.number().int().nonnegative().optional(), + IsPrimaryKey: z.boolean(), + IsNotNull: z.boolean(), + IsUnique: z.boolean(), + AutoIncrement: z.boolean(), + DefaultValue: z.string().optional(), + Comment: z.string().optional(), + EnumRef: z.string().optional(), // DataType=ENUM ise → Enum node Name + IsGenerated: z.boolean().optional(), + GeneratedExpression: z.string().optional(), +}).strict(); + +const ForeignKeySchema = z.object({ + Name: z.string().optional(), + Columns: z.array(z.string()).min(1), + ReferencesTable: z.string().min(1), + ReferencesColumns: z.array(z.string()).min(1), + OnDelete: z.enum(["CASCADE","RESTRICT","SET_NULL","NO_ACTION"]).default("NO_ACTION"), + OnUpdate: z.enum(["CASCADE","RESTRICT","SET_NULL","NO_ACTION"]).default("NO_ACTION"), +}).strict(); + +const IndexSchema = z.object({ + IndexName: z.string().min(1), + Columns: z.array(z.string()).min(1), + Type: z.enum(["BTree","Hash","GIN","GiST"]).default("BTree"), + IsUnique: z.boolean().default(false), + IsPartial: z.boolean().optional(), + WhereClause: z.string().optional(), +}).strict(); + +// properties: +{ + TableName: z.string().min(1), + Description: z.string().min(1), + Columns: z.array(ColumnSchema).min(1), + PrimaryKey: z.object({ Columns: z.array(z.string()).min(1) }).optional(), // composite PK + ForeignKeys: z.array(ForeignKeySchema).default([]), + UniqueConstraints: z.array(z.object({ Name: z.string().optional(), Columns: z.array(z.string()).min(1) })).default([]), + CheckConstraints: z.array(z.object({ Name: z.string().optional(), Expression: z.string().min(1) })).default([]), + Indexes: z.array(IndexSchema).default([]), +} +``` +Tek-kolon PK → `Column.IsPrimaryKey`; composite → `PrimaryKey.Columns`. Tek-kolon FK için `ForeignKeys[]` (inline `Column.IsForeignKey` kaldırılır, tüm FK'ler `ForeignKeys[]`'te toplanır — tutarlılık). + +### DTO +```ts +const FieldSchema = z.object({ + Name: z.string().min(1), + DataType: z.string().min(1), + IsRequired: z.boolean(), + IsArray: z.boolean(), + ValidationRules: z.array(z.object({ + Rule: z.enum(["Min","Max","MinLength","MaxLength","Email","Url","Regex","Pattern","Positive","Negative"]), + Value: z.string().optional(), + })).default([]), + DefaultValue: z.string().optional(), + NestedDTORef: z.string().optional(), // → DTO node Name + EnumRef: z.string().optional(), // → Enum node Name + Description: z.string().optional(), +}).strict(); +// properties: { Name, Description, Fields: array(FieldSchema).min(1) } +``` + +### Model / Entity +```ts +const PropertySchema = z.object({ + Name: z.string().min(1), + Type: z.string().min(1), + IsNullable: z.boolean().default(false), + IsCollection: z.boolean().default(false), + RelationType: z.enum(["OneToOne","OneToMany","ManyToOne","ManyToMany"]).optional(), + RelatedModelRef: z.string().optional(), // → Model node Name +}).strict(); + +const MethodSchema = z.object({ + MethodName: z.string().min(1), + Visibility: z.enum(["public","private","protected"]).default("public"), + Parameters: z.array(z.object({ + Name: z.string().min(1), Type: z.string().min(1), + Optional: z.boolean().default(false), Default: z.string().optional(), + })).default([]), + ReturnType: z.string().min(1), + IsAsync: z.boolean().default(false), + IsStatic: z.boolean().default(false), +}).strict(); +// properties: { ClassName, Description, TableRef? (→Table), Properties: min(1), Methods: default([]) } +``` + +### Enum +```ts +// properties: +{ + Name: z.string().min(1), + Description: z.string().min(1), + BackingType: z.enum(["string","int"]).default("string"), + Values: z.array(z.object({ + Key: z.string().min(1), + Value: z.string().optional(), // backing value (yoksa Key) + Description: z.string().optional(), + })).min(1), +} +``` +Plans "List of Strings/Key-Values" → key-value object'e yükseltilir. (Migration: eski `string[]` → `[{Key: s}]`.) + +### View +```ts +// properties: +{ + ViewName: z.string().min(1), + Description: z.string().min(1), + Definition: z.string().min(1), // SQL/aggregate + SourceTables: z.array(z.string()).min(1), // → Table Name'leri + Materialized: z.boolean(), + Columns: z.array(z.object({ Name: z.string().min(1), DataType: z.string().min(1) })).default([]), + RefreshStrategy: z.enum(["onDemand","scheduled","onChange"]).optional(), // materialized +} +``` + +## 4. Faz B — İş Mantığı + Erişim + +### Service +``` +ServiceName, Description, IsTransactionScoped +Methods[]: MethodName, Visibility, Parameters[] ({Name,Type,Optional,Default?,DtoRef?}), + ReturnType, ReturnDtoRef?, IsAsync, Throws[] (→Exception Name), Description? +Dependencies[]: { Kind: "Repository"|"Service"|"Cache"|"ExternalService", Ref: string } // DI +``` + +### Worker +``` +WorkerName, Description, Schedule (cron), TaskToExecute, TimeoutSeconds, +RetryPolicy: { MaxRetries, BackoffStrategy? (fixed/exponential), DelaySeconds? }, +Concurrency? (int), IsEnabled (default true) +``` + +### EventHandler +``` +HandlerName, Description, EventName, IsAsync, QueueRef? (→MessageQueue), +RetryPolicy? ({MaxRetries, DelaySeconds?}), DeadLetterQueue? (string) +``` + +### Orchestrator +``` +OrchestratorName, Description, Pattern (Saga/CompensatingTransaction/StateMachine/ProcessManager) +Steps[]: { StepName, ServiceRef (→Service), Action, CompensationAction?, OnFailure (retry/compensate/abort) } +``` + +### Controller (mevcut + zenginleştirme) +``` +ControllerName, Description, BaseRoute, Version? +Endpoints[]: HttpMethod, Route, RequestDTORef? (→DTO), ResponseDTORef? (→DTO), + RequiresAuth, RequiredRoles[], + PathParams[]? ({Name, Type}), QueryParams[]? ({Name, Type, Required}), + StatusCodes[]? ({Code, Description}), MiddlewareRefs[]? (→Middleware), + RateLimit? ({Requests, WindowSeconds}), Description? +``` + +### MessageQueue +``` +QueueName, Description, Type (Queue/Topic), Provider, MessageFormat (→DTO), +DeliveryGuarantee? (at-least-once/exactly-once/at-most-once), MaxRetries?, +DeadLetterQueue?, RetentionSeconds? +``` + +### APIGateway +``` +GatewayName, Description, Provider, AuthMode? (None/JWT/OAuth2/ApiKey), CorsEnabled? +Routes[]: { Path, TargetRef (→Controller/Service), Methods[], AuthRequired, RateLimit? } +``` + +## 5. Faz C — Altyapı + İstemci + Güvenlik + Konfig + Yapı + +### Repository +``` +RepositoryName, Description, EntityRef (→Model/Table), BaseClass?, IsCached (default false) +CustomQueries[]: { QueryName, QueryType (select/insert/update/delete/aggregate), + Parameters[]? ({Name, Type}), ReturnType? } +``` + +### Cache +``` +CacheName, Description, KeyPattern, TTL_Seconds, Engine (Redis/Memcached/Memory), +EvictionPolicy? (LRU/LFU/FIFO/TTL), MaxSizeMB?, Serialization? (JSON/MsgPack/Binary) +``` + +### ExternalService +``` +ServiceName, Description, BaseURL, AuthType, TimeoutSeconds, +Endpoints[]? : { Name, Method, Path, RequestFormat?, ResponseFormat? }, +RetryPolicy? ({MaxRetries, BackoffStrategy?}), RateLimit?, CircuitBreaker? ({FailureThreshold, ResetSeconds}) +``` + +### FrontendApp +``` +AppName, Description, Framework, DeploymentType, +StateManagement? (Redux/Zustand/Context/MobX/None), StylingApproach? (Tailwind/CSSModules/StyledComponents/...), +Routes[]? : { Path, ComponentRef (→UIComponent) } +``` + +### UIComponent (mevcut + tipleme) +``` +ComponentName, Description, +Props[]: { Name, Type, Required, Default? }, +State[]: { Name, Type, Initial? }, +Events[]? : { Name, PayloadType? }, +ChildComponentRefs[]? (→UIComponent) +``` + +### Middleware +``` +MiddlewareName, Description, AppliesTo (Global/SpecificRoutes), ExecutionOrder, +MiddlewareType? (auth/logging/ratelimit/cors/compression/validation/custom), +Config? (record) +``` + +### EnvironmentVariable +``` +Key, Description, DataType (String/Number/Boolean), IsSecret, Environment[] (Dev/Staging/Prod), +DefaultValue?, IsRequired (default true), ValidationPattern? (regex) +``` + +### Exception +``` +ExceptionName, Description, HttpStatusCode, LogSeverity (Info/Warning/Error/Critical), +ErrorCode? (örn ERR_AUTH_001), ParentExceptionRef? (→Exception, extends), Message? +``` + +### Module (Bounded Context) +``` +ModuleName, Description, StrictBoundaries, +ExposedServices[]? (→Service Name'leri — dışarı açık API), +Dependencies[]? (→Module Name'leri — DependsOn) +``` + +## 6. Altyapı Bileşenleri + +### Migration +- `src/nodes/schemas/version.ts`: `export const GRAPH_SCHEMA_VERSION = N;` Her faz bump. +- `src/neo4j/migrations/run.ts` zaten cypher dosyalarını çalıştırıyor; node-level veri dönüşümü için ayrı `tsx` migration script (`migrations/data/00X-enrich-.ts`): tüm node'ları çek, `properties` JSON parse, eksik zorunlu alanları default ile doldur, re-serialize, geri yaz. Idempotent. +- Mevcut DB az veri → migration düşük riskli. + +### UI Field Metadata +- Zod `.describe("...")` her alanda → `zodV3ToOpenAPI` JSON Schema `description`'a taşır. +- `node-types/registry.ts`'e opsiyonel `fieldHints: Record` (örn `IsPrimaryKey → badge:"PK"`, `IsForeignKey/FK → "FK"`). `node-types/:typeId` response'una `fieldHints` eklenir. + +### AI Prompt +- `src/ai/prompts/system-prompt.ts` node şema rehberi her faz sonunda güncellenir. Çıkış kriteri: AI canlı testte ilgili fazın node'larını ≤3 denemede üretebilmeli. + +### Test +- Her zenginleştirilen schema için `*.schema.spec.ts` valid + invalid (yeni zorunlu alanlar, enum'lar, nested). +- `nodes.service.spec` / `graph.service.spec` / e2e fixtures yeni şemaya güncellenir. +- `node-types.service.spec`: fieldHints + zengin schema. +- AI canlı doğrulama (faz sonunda). + +## 7. Faz Çıkış Kriterleri + +**Her faz için:** +1. İlgili node şemaları zenginleştirildi (required alanlar + .describe()). +2. `GRAPH_SCHEMA_VERSION` bump + migration script (mevcut node'lar dönüştürüldü). +3. `create-node.dto.ts` discriminated union güncel. +4. `node-types/registry.ts` fieldHints güncel. +5. `system-prompt.ts` node şema rehberi güncel. +6. Unit + service + e2e testleri yeşil. +7. AI canlı doğrulama: ilgili fazın node'ları AI ile ≤3 denemede üretilebiliyor. + +**Faz A** = Table/DTO/Model/Enum/View. **Faz B** = Service/Worker/EventHandler/Orchestrator/Controller/MessageQueue/APIGateway. **Faz C** = Repository/Cache/ExternalService/FrontendApp/UIComponent/Middleware/EnvironmentVariable/Exception/Module. + +## 8. Out of Scope + +- Ref-validation (EnumRef gerçekten var mı) — Phase 1.5 esnek; sonra eklenebilir. +- Gerçek codegen (DDL/entity üretimi) — Phase 5; bu spec sadece şemayı codegen-ready yapar. +- Frontend inspector UI implementasyonu — backend metadata sağlar, UI ayrı. +- Edge properties enrichment — bu spec sadece node properties (edge ayrı iş). + +## 9. Açık Notlar +- Cross-ref'ler isim string; codegen/resolve katmanı id'ye çevirir (gelecek). +- Composite PK + tek-kolon PK ikisi de desteklenir (tutarlılık için tek-kolon FK de `ForeignKeys[]`'e taşınır). +- Migration mevcut az veriyle düşük riskli; yine de idempotent + geri-okuma doğrulamalı. diff --git a/apps/server/docs/specs/2026-05-22-phase4-graphrag-design.md b/apps/server/docs/specs/2026-05-22-phase4-graphrag-design.md new file mode 100644 index 0000000..b4382d2 --- /dev/null +++ b/apps/server/docs/specs/2026-05-22-phase4-graphrag-design.md @@ -0,0 +1,163 @@ +# Phase 4 — Pattern Library + GraphRAG Tasarımı + +**Tarih:** 2026-05-22 +**Durum:** Onaylandı (tasarım), implementasyon bekliyor +**Önceki:** Phase 3B (AI agent), Node Enrichment Faz A/B/C (21 node v4 codegen-ready) + +## 1. Amaç + +AI mimar agent'ının (Phase 3B: Kimi K2.5 generation + DeepSeek chat) üretim +isabetini artırmak. Bunu, kullanıcının isteğine en yakın **kanonik mimari +desenleri** (pattern) semantik olarak getirip system prompt'a enjekte ederek +yaparız. Agent "sıfırdan tahmin" yerine "kanıtlanmış bir desene benzeterek" üretir. + +Bu, retrieval-augmented generation'ın (RAG) graf alanına uyarlanmış halidir: +corpus = yeniden kullanılabilir mimari alt-graflar. + +## 2. Kapsam kararları (brainstorming çıktısı) + +| Karar | Seçim | Gerekçe | +|-------|-------|---------| +| Corpus | Küratörlü **pattern/referans kütüphanesi** (alt-graf + açıklama + metadata) | Yeni kullanıcıda bile değer; agent'a "şuna benzet" zemini | +| Doluluk | **Seed + promote** | Hemen değer (seed) + organik büyüme (promote) | +| Vector store | **Neo4j native vector index** (5.13+ community) | Ekstra servis YOK; pattern'ler zaten graf dünyasında | +| Embedding sağlayıcı | **Bedrock** (bedrock-mantle, mevcut `BEDROCK_API_KEY`), provider-abstracted | LLM ile aynı abstraction, yeni anahtar yok | +| Agent entegrasyonu | **Otomatik enjeksiyon** (LLM turundan önce top-K) | Deterministik, ekstra tur yok, mevcut ReAct loop'a minimal dokunur | + +## 3. Veri modeli + +`:Pattern` node — proje node'larından izole (ayrı label, `:Node` değil): + +``` +:Pattern { + id: string (uuid), + name: string, + description: string, // semantik aramanın embed kaynağı (+ name + tags) + tags: string[], + graphJson: string, // serialized { nodes: [...], edges: [...] } alt-graf + embedding: float[], // description+name+tags vektörü + source: "seed" | "promoted", + createdAt: string (ISO) +} +``` + +Vektör index (migration ile): +```cypher +CREATE VECTOR INDEX pattern_embedding IF NOT EXISTS +FOR (p:Pattern) ON (p.embedding) +OPTIONS { indexConfig: { + `vector.dimensions`: $dim, // env EMBED_DIM (Titan v2 = 1024) + `vector.similarity_function`: 'cosine' +} } +``` + +`graphJson`, GraphService.apply'ın kabul ettiği `{nodes, edges}` (tempId tabanlı) +biçiminde tutulur — agent'a aynen verilebilir, gerekirse doğrudan apply edilebilir. + +## 4. Modüller + +### `src/embeddings/` — embedding provider abstraction +- `embeddings.factory.ts` — `llm.factory.ts` ikizi. `getEmbedder()` → + bedrock-mantle'a (OpenAI-uyumlu) bağlı `OpenAIEmbeddings` (mevcut + `BEDROCK_API_KEY` + `BEDROCK_BASE_URL`). Env: `EMBED_MODEL`, `EMBED_DIM`. +- `embeddings.service.ts` — `embed(text: string): Promise`, + `embedBatch(texts): Promise`. Sağlayıcı yoksa (`isConfigured()=false`) + çağıranlar graceful davranır. + +### `src/patterns/` — pattern kütüphanesi +- `schemas/pattern.schema.ts` — Zod: `PatternSchema`, `CreatePatternSchema` + (id/embedding/createdAt server üretir), `SearchPatternSchema`. +- `patterns.repository.ts` — Cypher CRUD + `search(vector, k, minScore)` + (`db.index.vector.queryNodes('pattern_embedding', k, $vec)` + skor filtresi). +- `patterns.service.ts` — create (embed + store), list, getById, delete, + search (embed query + repo.search), promoteFromProject. +- `patterns.controller.ts` — aşağıdaki endpoint'ler. +- `patterns.module.ts` — EmbeddingsModule + Neo4jModule + ProjectsModule (promote için) import eder. + +### Entegrasyon noktaları +- `src/ai/ai.service.ts` — chat akışında retrieval + enjeksiyon (Bölüm 6). +- `src/ai/prompts/system-prompt.ts` — `buildSystemPrompt(graph, patterns?)` — + yeni opsiyonel `patterns` parametresi + "İLGİLİ REFERANS DESENLER" bölümü. +- `src/neo4j/migrations/` — vektör index migration. +- Seed script: `scripts` veya `src/patterns/seed/` + `pnpm seed:patterns`. + +## 5. API + +| Endpoint | Body | Dönüş | +|----------|------|-------| +| `POST /patterns` | `{ name, description, tags?, graphJson }` | oluşturulan Pattern (embedding hariç özet) | +| `GET /patterns` | — | Pattern özet listesi | +| `GET /patterns/:id` | — | tek Pattern (graphJson dahil) | +| `DELETE /patterns/:id` | — | 204 | +| `POST /patterns/search` | `{ query, k?, minScore? }` | `[{ pattern, score }]` top-K | +| `POST /projects/:id/patterns/promote` | `{ name, description, tags?, nodeIds? }` | Pattern (nodeIds verilmezse tüm proje grafiği) | + +`pnpm seed:patterns` — ~10-15 kanonik pattern (JWT auth akışı, katmanlı CRUD, +Saga ödeme, CQRS read model, cache-aside, event-driven handler, API gateway +routing, repository + custom query, DTO validation katmanı, exception hiyerarşisi). +Her pattern: name + description + graphJson (enriched v4 node şemalarıyla). Idempotent +(aynı isim varsa atla). + +## 6. Retrieval akışı (otomatik enjeksiyon) + +`AiService.chat(projectId, body)` içinde, `current_graph` yüklendikten sonra, +LLM turundan **önce**: + +1. `embeddings.isConfigured()` ise → `vec = embed(body.message)`. +2. `patterns = patternsRepo.search(vec, k=3, minScore=0.7)`. +3. `systemPrompt = buildSystemPrompt(graph, patterns)`. +4. Prompt'a eklenen bölüm: + ``` + ## İLGİLİ REFERANS DESENLER (retrieval) + Aşağıdaki kanıtlanmış desenler isteğine yakın. Uygunsa bunlara benzeterek üret: + - [pattern.name] (skor 0.xx): pattern.description + Yapı: + ``` +5. Embedding sağlayıcı yok / index yok / sonuç boş → bölüm hiç eklenmez, agent + normal çalışır. + +Enjeksiyon **ilk turda** yapılır (kullanıcı mesajına göre). Self-correction +turlarında tekrar retrieval yapılmaz (mesaj değişmedi). + +## 7. Hata yönetimi + +- **Embedding sağlayıcı yok**: retrieval atlanır, agent pattern'siz çalışır + (mevcut davranış). Loglanır, hata fırlatılmaz — retrieval bir *enhancement*. +- **bedrock-mantle `/embeddings` yoksa**: lokal embedder'a (Xenova/transformers.js, + `EMBED_PROVIDER=local`) düşülür. **Bu uygulamanın İLK adımında doğrulanır** + (embedding boyutunu → vektör index'i etkiler). +- **Vektör index yok**: `search` boş döner (migration çalıştırılmamış uyarısı log). +- **graphJson geçersiz**: pattern create sırasında `{nodes, edges}` şema doğrulaması + (Zod) — geçersizse `ERR_PATTERN_INVALID_GRAPH`. +- **promote'ta boş/eksik nodeIds**: proje yoksa `ERR_PROJECT_NOT_FOUND`; nodeIds + projede yoksa `ERR_PATTERN_NODE_NOT_FOUND`. + +## 8. Test stratejisi + +- **Unit**: PatternSchema (Zod kabul/red), patterns.service (mock repo + mock + embedder), embeddings.factory (configured/not), buildSystemPrompt pattern + enjeksiyonu (string içerik kontrolü), promoteFromProject (mock graph). +- **e2e** (Testcontainers Neo4j 5-community): deterministik **fake embedder** + enjekte (sabit vektör fonksiyonu) → vektör index migration → `:Pattern` yaz → + `POST /patterns/search` top-K + skor doğru → `promote` round-trip. Gerçek + embedding API'ye bağlanmadan vector index Cypher'ı doğrulanır. +- **Canlı** (gerçek Bedrock): `seed:patterns` → bir pattern'e uyan istek → AI + yanıtında o desenin enjekte edildiği + çıktıyı etkilediği doğrulanır. + +## 9. Out of scope (sonraki fazlar) + +- Pattern versiyonlama / güncelleme (sadece create+delete; update sonra). +- Otomatik promote tetikleyici (UX-driven; v1 manuel endpoint). +- Çok-kullanıcılı pattern izolasyonu / sahiplik (şu an global kütüphane). +- Embedding cache / re-embed batch job (pattern create'te tek seferlik embed yeter). +- Cross-encoder re-ranking (top-K cosine yeter; gerekirse sonra). +- Codegen entegrasyonu (Phase 5). + +## 10. Açık notlar + +- Embedding boyutu modele bağlı (Titan v2=1024, Cohere=1024, OpenAI-3-small=1536). + `EMBED_DIM` env + vektör index aynı değeri kullanmalı. Model değişirse re-embed + + index yeniden oluşturma gerekir (migration not'u). +- `graphJson` GraphService.apply girdi formatıyla aynı → ileride "pattern'i + doğrudan uygula" özelliği bedavaya gelir. +- Provider abstraction sayesinde lokal/Bedrock/OpenAI arası geçiş tek factory'de. diff --git a/apps/server/docs/specs/2026-05-22-tabs-contexts-design.md b/apps/server/docs/specs/2026-05-22-tabs-contexts-design.md new file mode 100644 index 0000000..ebe68e9 --- /dev/null +++ b/apps/server/docs/specs/2026-05-22-tabs-contexts-design.md @@ -0,0 +1,119 @@ +# Tabs / Contexts Tasarımı + +**Tarih:** 2026-05-22 +**Durum:** Onaylandı (tasarım), implementasyon bekliyor +**Amaç:** Frontend'in çoklu-sekme (Obsidian-tarzı bağlam) UX'ini desteklemek — proje +içinde birden fazla canvas (sekme), kutuların sekmeler arası **referans (import)** ile +paylaşımı. + +## 1. Çekirdek model — "tek ev + referanslar" + +Benzetme: bir kutu = bir component. Component **tek bir yerde tanımlıdır** (evi), başka +yerlerde **import** edilerek kullanılır. Import bir kopya değil, aynı şeyi işaret eden +bir referanstır; datasını bir yerde değiştirince her yerde değişir (tek kaynak). + +- Her **node**'un bir **evi** vardır: bir `homeTab` + o sekmedeki konumu (`position`). + Node oluşturulduğu sekmede "tanımlanır". +- Node, ev sekmesinde otomatik kendi `position`'ında görünür. +- Node başka bir sekmede **referans (import/kısayol)** olarak gösterilebilir: o sekmede + kendi konumuyla belirir, "referans" olarak işaretlidir ve kaynağı (ev sekmesi) görünür. +- **Tek kaynak:** node datası (properties) düzenlenince ev + tüm referanslarda aynı anda + güncel. Referans yalnızca konum/görünüm; kimlik ve data tektir. +- **Rules Engine global kalır:** kurallar mantıksal graf (node + edge) üzerinde işler, + sekmeden bağımsız. Referansa ok çekmek aynı kurallarla doğrulanır. +- **v1 kapsam:** yalnızca context/canvas sekmeleri. View sekmeleri + (Infrastructure/DBSchema/Flow filtreli görünümler) sonraki faz (out of scope). + +## 2. Veri modeli (Neo4j) + +``` +(:Tab { + id, projectId, name, isDefault, order, moduleNodeId?, createdAt, updatedAt +}) + +:Node → mevcut alanlar KORUNUR (positionX/positionY, properties, type, projectId, ...) + + yeni alan: homeTabId // node'un ev sekmesi + +(:Tab)-[:REFERENCES { x, y }]->(:Node) // ev DIŞI sekmelerdeki import/kısayol +``` + +- `isDefault=true` → otomatik "Ana Mimari" sekmesi (her projede bir tane, silinemez). +- `moduleNodeId?` → drill-down kaynağı Module node bağı (opsiyonel; çift tık ile bir + Module'ün içine girince açılan sekme onunla ilişkilenir). +- `order` → sekme çubuğu sırası. +- Node `position` (ev konumu) ve `homeTabId` node üzerinde durur; referans konumu + `REFERENCES` ilişkisinde (`x, y`). + +**Bir sekmenin içeriği (render):** +- `homeTabId = sekme` olan node'lar → kendi `position`'larında (sahip/owned). +- O sekmeye `REFERENCES` ile bağlı node'lar → ilişkideki `x, y`'de (referans/imported, + origin = node.homeTabId). +- Edge'ler: iki ucu da o sekmede görünen (owned veya referenced) edge'ler çizilir. + +## 3. API yüzeyi (çoğu additive — mevcut bozulmuyor) + +**Yeni Tabs modülü:** +| Endpoint | İş | +|---|---| +| `POST /projects/:id/tabs` | sekme oluştur `{ name, moduleNodeId? }` | +| `GET /projects/:id/tabs` | sekme listesi | +| `GET /projects/:id/tabs/:tabId` | sekme detayı (owned + referenced üyeler, pozisyon, origin) | +| `GET /projects/:id/tabs/:tabId/graph` | render içeriği: pozisyonlu node'lar + aralarındaki edge'ler | +| `PATCH /projects/:id/tabs/:tabId` | yeniden adlandır / sırala | +| `DELETE /projects/:id/tabs/:tabId` | sekme sil (default silinemez; owned node'ların evi default'a taşınır, referanslar kaldırılır) | +| `PUT /projects/:id/tabs/:tabId/references/:nodeId` | node'u sekmeye import et / referans konumunu güncelle `{ x, y }` | +| `DELETE /projects/:id/tabs/:tabId/references/:nodeId` | referansı kaldır (node silinmez) | +| `PATCH /projects/:id/tabs/:tabId/layout` | toplu konum kaydet `[{ nodeId, x, y }]` (canvas drag perf; owned → node.position, referenced → REFERENCES.x/y) | + +**Mevcut endpoint'lerde küçük değişiklik:** +- **Node create** (`POST /projects/:id/nodes`): opsiyonel `homeTabId` (verilmezse projenin + default sekmesi). `position` aynen alınır (ev konumu). Node + homeTab ataması. +- **graph/apply** + **ai/chat**: opsiyonel `tabId` (üretilen node'ların `homeTabId`'si; + verilmezse default sekme). Grid konumları node.position'a yazılır (bugünküyle aynı). +- **GET /projects/:id/graph**: DEĞİŞMEZ — mantıksal graf + node.position (ev konumu) + döner. AI ve pattern-promote pozisyon kullanmadığından etkilenmez. + +## 4. Migration (yıkıcı değil) + +`005-tabs.ts` (idempotent): +- Her projeye `:Tab { name:"Ana Mimari", isDefault:true, order:0 }` oluştur (yoksa). +- Her node'un `homeTabId`'sini o default sekmeye ata (yoksa). +- node.position KORUNUR (silinmez). Tekrar çalıştırmak güvenli. +- Constraint: `tab_id_unique`. + +## 5. Kenar durumlar / kurallar + +- Bir node birden çok sekmede referans edilebilir; evi tektir. +- Default sekme silinemez (`ERR_TAB_DEFAULT_DELETE`). +- Sekme silinince: owned node'ların `homeTabId`'si default sekmeye taşınır (node kaybolmaz), + o sekmedeki REFERENCES'lar kaldırılır. +- Node silinince (mevcut cascade genişler): node + tüm REFERENCES'ları + edge'leri silinir. +- Aynı node'u kendi ev sekmesine referans olarak eklemek reddedilir (`ERR_TAB_SELF_REFERENCE`). +- Olmayan sekme/node: `ERR_TAB_NOT_FOUND` / `ERR_NODE_NOT_FOUND`. + +## 6. Moduller + test + +- Yeni `src/tabs/` (schema/repo/service/controller/module), ProjectsModule/NodesModule + desenini izler; ProjectsRepository (proje var mı) + NodesRepository (node var mı) inject. +- Etkilenen: nodes (homeTabId), graph (apply tabId), ai (tabId) — küçük dokunuşlar. +- **Test:** + - Unit: tab schema (Zod), tabs repo (Cypher mock — tab CRUD + reference upsert/remove + + tab graph), tabs service (default tab koruması, sekme silince home taşıma, self-reference + reddi), node create homeTabId default. + - E2E (Testcontainers): sekme CRUD + node import (reference) + tab graph (owned+referenced) + + sekme silince home taşıma + migration round-trip. Mevcut node/apply e2e'leri homeTabId + ile güncelle. + +## 7. Out of scope (sonraki fazlar) + +- View sekmeleri (Infrastructure/DBSchema/Flow filtreli, auto-layout). +- Semantik zoom / Makro-Graf (Nexus) görünümü — frontend render konusu, backend verisi yeter. +- Sekme bazlı erişim/paylaşım. + +## 8. Açık notlar + +- node.position korunduğu için tüm mevcut akışlar (apply/AI/CRUD/graph) çalışmaya devam eder; + bu faz büyük ölçüde **additive**. +- Frontend (custom Canvas 2D) bir sekmeyi `GET /tabs/:id/graph` ile çeker; referans node'ları + görsel olarak "import" rozetiyle (origin) gösterir. +- `moduleNodeId` v1'de yalnızca saklanır; drill-down otomasyonu frontend'de. diff --git a/apps/server/docs/specs/2026-06-12-graph-revision-and-cli-push.md b/apps/server/docs/specs/2026-06-12-graph-revision-and-cli-push.md new file mode 100644 index 0000000..b769f93 --- /dev/null +++ b/apps/server/docs/specs/2026-06-12-graph-revision-and-cli-push.md @@ -0,0 +1,129 @@ +# Graf Revizyonu + CLI Push — SOLARCH 2.0 Faz 2 tasarımı + +**Tarih:** 2026-06-12 · **Durum:** Uygulandı + +## Problem + +Solarch grafı artık üç istemciden yazılıyor: web canvas (insan), AI agent ve +`solarch push` (CLI). Node seviyesinde optimistic locking vardı +(`version` + `expectedVersion`), ama **graf seviyesinde** revizyon yoktu — iki +istemci aynı anda node/edge eklerse kimse fark etmiyordu. Ayrıca `graph/apply` +yalnız batch içi `tempId`'ler arasına edge kurabiliyordu; CLI'ın "koddaki yeni +servis, buluttaki mevcut tabloya bağlanacak" senaryosu imkânsızdı. + +## Tasarım: iki katmanlı çatışma koruması + +``` +Katman 1 — graf revizyonu : "graf bütünü değişti mi?" → push'un rebase tetiği +Katman 2 — node versiyonu : "şu tek node değişti mi?" → property PATCH guard'ı +``` + +### Katman 1: `Project.graphRevision` + +- Migration `006_graph_revision.cypher`: mevcut projelere `graphRevision = 0` backfill. +- `ProjectsRepository.bumpRevision(id)`: `SET p.graphRevision = coalesce(p.graphRevision,0)+1`. +- **Bump noktaları:** `NodesService` create/delete + properties değiştiren update, + `EdgesService` create/update/delete, `GraphService.apply` commit'i. +- **Bump ETMEYENLER (bilinçli):** salt pozisyon taşıma ve tab layout kaydetme — + çizimi taşımak mimariyi değiştirmez; bump'lasaydı her sürükleme CLI push'una + sahte çatışma üretirdi. +- `GET /projects/:id/graph` yanıtına `graphRevision` eklendi (`ProjectGraph` tipi). + +### Katman 2: `Node.version` (mevcuttu, değişmedi) + +`PATCH /nodes/:id` + `expectedVersion`; uyuşmazlıkta `409 ERR_VERSION_CONFLICT` ++ `currentVersion`. `ConflictFilter` artık `currentRevision`'ı da zarfa taşır. + +## `graph/apply` genişlemesi (upsert köprüsü) + +### Edge uçları: tempId VEYA cloud id + +```jsonc +{ + "baseRevision": 5, // opsiyonel — çatışma koruması + "mutations": { + "nodes": [{ "tempId": "t_svc", "type": "Service", "properties": { … } }], + "edges": [ + { "sourceTempId": "t_svc", "targetId": "ca33ceae-…", "edgeType": "CALLS" } + // yeni node ↑ ↑ mevcut cloud node (UUID) + ] + } +} +``` + +- Zod şeması her uç için **tam olarak birini** zorlar (`sourceTempId` XOR `sourceId`). +- Mevcut node DB'den okunur ve **Rules Engine'e verilir** — upsert köprüsü kural + denetimini baypas etmez. Bulunamayan id → `ERR_EDGE_NODE_NOT_FOUND` (rollback). +- Edge yazımı `apoc.create.relationship` yerine **`apoc.merge.relationship`**: + aynı `(source, target, kind, projectId)` ikinci kez gönderilirse yeni kayıt + açılmaz → push **idempotent**. `createdAt/updatedAt` yalnız ilk yaratımda set + edilir (`coalesce`). + +### `baseRevision` sözleşmesi + +1. **Ön kontrol:** validasyonlardan önce revizyon karşılaştırılır — eskiyse + hiçbir iş yapılmadan `409 ERR_GRAPH_REVISION_CONFLICT` + `currentRevision`. +2. **Atomik kontrol:** asıl garanti commit transaction'ındadır — + `WHERE rev = $baseRevision` koşullu bump; araya yazma girdiyse 0 kayıt döner, + transaction rollback olur ve aynı 409 fırlar (TOCTOU yarışı kapalı). +3. Commit sonunda bump yapılır; yanıt `graphRevision` döner — istemci bir + sonraki push'ta bunu `baseRevision` olarak kullanır. +4. `baseRevision` verilmezse davranış eskisi gibi (AI/UI çağrıları kırılmaz). +5. Boş mutation no-op'tur: bump yok, mevcut revizyon döner. + +## CLI tarafı + +### `solarch pull` + +`GET graph` → `.solarch/to-be.json` (revizyon dahil). Offline `diff --to-be` +ve push öncesi referans. + +### `solarch push` akışı + +``` +taze GET graph (rev R) ─▶ diff ─▶ plan { yeni node'lar, yeni edge'ler, liste-property farkları } + illegal edge varsa ──▶ exit 1 (ASLA pushlanmaz) + plan boşsa ─────────▶ "Already in sync" (no-op) + onay (--yes CI'da atlar) + graph/apply (baseRevision=R) + 409 ─▶ re-pull + re-plan + TEK retry ─▶ yine 409 ─▶ kullanıcıya bırak + PATCH /nodes/:id (expectedVersion) — liste alanlarında KOD kaynak kabul edilir + 409 ─▶ TTY: cloud'u tut / kodu yaz / atla · CI: otomatik atla + rapor + idMap ─▶ .solarch/map.json (yeni node'lar anında eşleşmiş) +``` + +Property merge semantiği: yalnız liste alanları (Columns/Fields/Methods/ +Endpoints/Values) kodunkiyle değiştirilir; cloud'da elle yazılmış diğer +property'ler (Description vb.) **korunur**. + +## Hata kodları + +| Kod | HTTP | Kaynak | İstemci tepkisi | +|---|---|---|---| +| `ERR_GRAPH_REVISION_CONFLICT` | 409 | `graph/apply` + `baseRevision` | re-pull + re-plan + tek retry | +| `ERR_VERSION_CONFLICT` | 409 | `PATCH /nodes/:id` + `expectedVersion` | interaktif: cloud/kod/atla | +| `ERR_EDGE_NODE_NOT_FOUND` | 200 (violations) | apply'da cloud id bulunamadı | rollback raporu | + +## Kapsam dışı (bilinçli) + +- **Silme/`--prune` yok** — cloud'dan node silmek yalnız canvas'tan yapılır. +- Frontend'e canlı "başkası değiştirdi" bildirimi yok (React Query refetch + yeterli); UI işi Faz 4'e. + +## Doğrulama + +- `graph.service.spec.ts`: mevcut-node edge, revizyon çatışması (ön kontrol + + tx-içi), idempotent merge, boş mutation no-op. +- CLI `test/push.test.ts`: planner (tempId/cloudId karışık uçlar, illegal edge + dışlama, property merge), 409 retry akışı (API mock). +- Uçtan uca smoke (lokal backend + Neo4j): fixture app push → 15 node + 17 edge; + ikinci push no-op; buluttan silinen method üçüncü push'ta koddan geri yazıldı. + +## Yan bulgular (bu çalışmada düzeltildi) + +- Migration koşucusu `--` yorumla başlayan cypher bloklarını sessizce + atlıyordu (005'in ilk constraint'i hiç çalışmamıştı) — yorum satırları artık + statement içinden ayıklanıyor. +- AST extractor'ları Exception/Middleware/Worker/EventHandler/Orchestrator + için zorunlu şema alanlarını üretmiyordu — şema-geçerli default'lar eklendi + (yoksa push şema kontrolüne takılıyordu). diff --git a/apps/server/docs/superpowers/plans/2026-06-29-api-docs.md b/apps/server/docs/superpowers/plans/2026-06-29-api-docs.md new file mode 100644 index 0000000..943260b --- /dev/null +++ b/apps/server/docs/superpowers/plans/2026-06-29-api-docs.md @@ -0,0 +1,392 @@ +# API Docs (AI Documentize) — Implementation Plan (Plan 1 of 2) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax. + +**Goal:** Turn the architecture graph into interactive Scalar API documentation: a deterministic OpenAPI from the graph, an "AI Documentize" enrichment (prose + examples only), an in-app "API" surface (Scalar render + manual localhost test), and `@nestjs/swagger` + Scalar emitted into the generated app so it self-documents. + +**Architecture:** `projectOpenApi(graph)` deterministically emits OpenAPI 3.1 (mirrors `simple-projection.ts`). `apiDoc(projectId, stage?, force?)` serves baseline (instant) or AI-enriched (cached on the Project node, mirrors `simpleSketchModel`). Codegen emitters add swagger decorators + Scalar at `/docs`. Frontend adds a 3rd `"api"` surface rendering Scalar via `@scalar/api-reference`. + +**Tech Stack:** NestJS, Neo4j, `@nestjs/swagger@11.4.4` (`OpenAPIObject` type), `@scalar/nestjs-api-reference@1.1.17` (generated app), `@scalar/api-reference` (frontend — NEW dep), LangChain (DeepSeek tool-calling), React + TanStack Query. + +**Scope of Plan 1:** docs + manual localhost test (browser-direct "Server URL"). **Plan 2** (separate) adds the VS Code local-deploy + proxy bridge + pairing for the privacy-clean automated test path. + +## Global Constraints + +- Code (comments + UI strings) **English only**; user comms Turkish (no code effect). +- No emojis; no AI-slop UI (no gradients/glassmorphism/pills); orange-bg text = ink (`#0e0e10`). +- Builds/tests: `export PATH="/run/host/usr/bin:/usr/bin:$HOME/.local/share/pnpm:$PATH"` first. **git uses DEFAULT PATH** (host PATH breaks git-remote-https). +- Commits → `solarch-dev` repos → **NO `Co-Authored-By`/Claude trailer**. +- Backend tests `pnpm vitest run ` + build `pnpm build` (nest build) in `solarch-backend`. Frontend build `pnpm build` (vite) in `solarch-frontend`. +- OpenAPI structure is **deterministic/verified** from the graph; AI only adds prose/examples on EXISTING operations/schemas (grounded; never invents paths). Mirror the Simple View AI-enrich guardrail. +- Edge identity facts: `propsOf<"Controller">(node)` / `propsOf<"DTO">` etc. for typed props; `graph.allOf(kind)`, `graph.resolveRef(kind,name)`, `graph.byId`. `OpenAPIObject` imported from `@nestjs/swagger`. + +--- + +### Task 1: OpenAPI emitter — paths/operations + +**Files:** +- Create: `solarch-backend/src/codegen/openapi.emitter.ts` +- Test: `solarch-backend/src/codegen/openapi.emitter.spec.ts` + +**Interfaces:** +- Produces: `projectOpenApi(graph: CodeGraph): OpenAPIObject` (this task: `info`, `paths`, `tags`, `security`; Task 2 adds `components.schemas`). +- Consumes: `CodeGraph` (`allOf`, `resolveRef`), `propsOf<"Controller">`, `OpenAPIObject` from `@nestjs/swagger`. + +- [ ] **Step 1: Write the failing test** + +```typescript +// solarch-backend/src/codegen/openapi.emitter.spec.ts +import { describe, it, expect } from "vitest"; +import { buildCodeGraph } from "./ir"; +import { projectOpenApi } from "./openapi.emitter"; +import type { StoredNode } from "../nodes/nodes.repository"; + +let seq = 0; +const uuid = () => `00000000-0000-4000-8000-${String(++seq).padStart(12, "0")}`; +function node(type: StoredNode["type"], properties: Record): StoredNode { + return { id: uuid(), type, projectId: "p", positionX: 0, positionY: 0, homeTabId: "t", + createdAt: "2026-06-29T00:00:00.000Z", updatedAt: "2026-06-29T00:00:00.000Z", version: 1, properties }; +} +function fixture() { + const ctrl = node("Controller", { + ControllerName: "UsersController", Description: "User ops", BaseRoute: "/users", + Endpoints: [ + { HttpMethod: "POST", Route: "/", RequestDTORef: "CreateUserDto", ResponseDTORef: "UserDto", RequiresAuth: true, RequiredRoles: [], PathParams: [], QueryParams: [], StatusCodes: [{ Code: 201, Description: "Created" }] }, + { HttpMethod: "GET", Route: "/:id", ResponseDTORef: "UserDto", RequiresAuth: false, PathParams: [{ Name: "id", DataType: "string" }], QueryParams: [], StatusCodes: [] }, + ], + }); + return buildCodeGraph([ctrl], []); +} + +describe("projectOpenApi — paths", () => { + it("emits an operation per endpoint with method, full path, params, security, tags", () => { + const doc = projectOpenApi(fixture()); + expect(doc.openapi).toMatch(/^3\.1/); + expect(doc.paths["/users"]?.post).toBeTruthy(); + expect(doc.paths["/users/{id}"]?.get).toBeTruthy(); + const post = doc.paths["/users"]!.post!; + expect(post.tags).toContain("UsersController"); + expect(post.security?.length).toBeGreaterThan(0); // RequiresAuth → security + expect((post.responses as Record)["201"]).toBeTruthy(); + const get = doc.paths["/users/{id}"]!.get!; + expect(get.parameters?.some((p) => (p as { name: string }).name === "id")).toBe(true); + expect(get.security ?? []).toHaveLength(0); // public + }); +}); +``` + +- [ ] **Step 2: Run test, verify it fails** + +Run: `cd solarch-backend && export PATH="/run/host/usr/bin:/usr/bin:$HOME/.local/share/pnpm:$PATH" && pnpm vitest run src/codegen/openapi.emitter.spec.ts` +Expected: FAIL — `projectOpenApi` not found. + +- [ ] **Step 3: Implement paths** + +```typescript +// solarch-backend/src/codegen/openapi.emitter.ts +import type { OpenAPIObject } from "@nestjs/swagger"; +import { propsOf, type CodeGraph, type CodeNode } from "./ir"; +import { pascalCase } from "./naming"; + +/** Convert a Nest-style path (BaseRoute + Route, ":id") to an OpenAPI path ("{id}"). */ +function fullPath(base: string, route: string): string { + const join = `${base ?? ""}/${route ?? ""}`.replace(/\/+/g, "/").replace(/\/$/, "") || "/"; + return join.replace(/:([A-Za-z0-9_]+)/g, "{$1}"); +} + +export function projectOpenApi(graph: CodeGraph): OpenAPIObject { + const paths: Record> = {}; + const tags: { name: string; description?: string }[] = []; + + for (const ctrl of graph.allOf("Controller")) { + const p = propsOf<"Controller">(ctrl); + tags.push({ name: ctrl.name, description: p.Description }); + for (const ep of p.Endpoints ?? []) { + const path = fullPath(p.BaseRoute, ep.Route); + const method = ep.HttpMethod.toLowerCase(); + const parameters = [ + ...(ep.PathParams ?? []).map((pp) => ({ name: pp.Name, in: "path", required: true, schema: { type: "string" } })), + ...(ep.QueryParams ?? []).map((qp) => ({ name: qp.Name, in: "query", required: false, schema: { type: "string" } })), + ]; + const responses: Record = {}; + const codes = (ep.StatusCodes ?? []).length ? ep.StatusCodes! : [{ Code: ep.HttpMethod === "POST" ? 201 : 200, Description: "OK" }]; + for (const sc of codes) { + responses[String(sc.Code)] = { + description: sc.Description ?? "Response", + ...(ep.ResponseDTORef && sc.Code < 400 + ? { content: { "application/json": { schema: ep.ReturnsCollection + ? { type: "array", items: { $ref: `#/components/schemas/${pascalCase(ep.ResponseDTORef)}` } } + : { $ref: `#/components/schemas/${pascalCase(ep.ResponseDTORef)}` } } } } + : {}), + }; + } + const op: Record = { + operationId: `${ctrl.name}_${method}_${path}`.replace(/[^A-Za-z0-9]+/g, "_"), + tags: [ctrl.name], + summary: ep.Description ?? `${ep.HttpMethod} ${path}`, + parameters, + responses, + ...(ep.RequiresAuth ? { security: [{ bearer: [] }] } : {}), + ...(ep.RequestDTORef ? { requestBody: { required: true, content: { "application/json": { schema: { $ref: `#/components/schemas/${pascalCase(ep.RequestDTORef)}` } } } } } : {}), + }; + (paths[path] ??= {})[method] = op; + } + } + + return { + openapi: "3.1.0", + info: { title: "API", version: "1.0.0" }, + tags, + paths: paths as OpenAPIObject["paths"], + components: { securitySchemes: { bearer: { type: "http", scheme: "bearer", bearerFormat: "JWT" } }, schemas: {} }, + }; +} +``` + +- [ ] **Step 4: Run test, verify it passes** — `pnpm vitest run src/codegen/openapi.emitter.spec.ts` → PASS. +- [ ] **Step 5: Commit** + +```bash +git add src/codegen/openapi.emitter.ts src/codegen/openapi.emitter.spec.ts +git commit -m "feat(openapi): deterministic graph->OpenAPI paths emitter" +``` + +--- + +### Task 2: OpenAPI emitter — component schemas (DTO/Model/Enum) + +**Files:** Modify `openapi.emitter.ts` + `openapi.emitter.spec.ts`. + +**Interfaces:** Produces `doc.components.schemas[PascalName]` for every DTO/Model/Enum; field → JSON Schema (DataType→type, IsArray→array, IsRequired→required[], ValidationRules→min/max/format/pattern, EnumRef→`$ref`, NestedDTORef→`$ref`). Consumes `propsOf<"DTO">`/`<"Model">`/`<"Enum">`. + +- [ ] **Step 1: Failing test (append)** + +```typescript +describe("projectOpenApi — component schemas", () => { + it("emits a schema per DTO with typed/required fields, enum and nested $refs", () => { + const dto = node("DTO", { Name: "CreateUserDto", Description: "New user", Fields: [ + { Name: "email", DataType: "string", IsRequired: true, IsArray: false, ValidationRules: [{ Rule: "Email" }] }, + { Name: "role", DataType: "string", IsRequired: false, IsArray: false, EnumRef: "UserRole" }, + { Name: "tags", DataType: "string", IsRequired: false, IsArray: true, ValidationRules: [] }, + ] }); + const en = node("Enum", { Name: "UserRole", Values: [{ Key: "ADMIN" }, { Key: "USER" }] }); + const graph = buildCodeGraph([dto, en], []); + const doc = projectOpenApi(graph); + const s = doc.components!.schemas!["CreateUserDto"] as { type: string; required?: string[]; properties: Record }; + expect(s.type).toBe("object"); + expect(s.required).toContain("email"); + expect(s.required ?? []).not.toContain("role"); + expect(s.properties.email.format).toBe("email"); + expect(s.properties.role.$ref).toBe("#/components/schemas/UserRole"); + expect(s.properties.tags.type).toBe("array"); + expect(doc.components!.schemas!["UserRole"]).toBeTruthy(); // enum schema present + }); +}); +``` + +- [ ] **Step 2: Run, verify fail** → schemas empty. +- [ ] **Step 3: Implement** — add before the `return` in `projectOpenApi`: + +```typescript + const schemas: Record = {}; + const DT: Record = { + string: { type: "string" }, int: { type: "integer" }, integer: { type: "integer" }, + number: { type: "number" }, float: { type: "number" }, boolean: { type: "boolean" }, + bool: { type: "boolean" }, date: { type: "string", format: "date-time" }, + datetime: { type: "string", format: "date-time" }, uuid: { type: "string", format: "uuid" }, + }; + const ruleToSchema = (rule: string, value?: string): Record => { + switch (rule) { + case "Min": return { minimum: Number(value) }; case "Max": return { maximum: Number(value) }; + case "MinLength": return { minLength: Number(value) }; case "MaxLength": return { maxLength: Number(value) }; + case "Email": return { format: "email" }; case "Url": return { format: "uri" }; + case "Regex": return value ? { pattern: value } : {}; default: return {}; + } + }; + for (const dto of graph.allOf("DTO")) { + const dp = propsOf<"DTO">(dto); + const properties: Record = {}; + const required: string[] = []; + for (const f of dp.Fields ?? []) { + let schema: Record; + if (f.EnumRef) { const e = graph.resolveRef("Enum", f.EnumRef); schema = { $ref: `#/components/schemas/${pascalCase(e ? e.name : f.EnumRef)}` }; } + else if (f.NestedDTORef) { const n = graph.resolveRef("DTO", f.NestedDTORef); schema = { $ref: `#/components/schemas/${pascalCase(n ? n.name : f.NestedDTORef)}` }; } + else { schema = { ...(DT[f.DataType.toLowerCase()] ?? { type: "string" }) }; for (const r of f.ValidationRules ?? []) Object.assign(schema, ruleToSchema(r.Rule, r.Value)); } + properties[f.Name] = f.IsArray ? { type: "array", items: schema } : schema; + if (f.IsRequired) required.push(f.Name); + if (f.Description) (properties[f.Name] as Record).description = f.Description; + } + schemas[pascalCase(dto.name)] = { type: "object", properties, ...(required.length ? { required } : {}), ...(dp.Description ? { description: dp.Description } : {}) }; + } + for (const en of graph.allOf("Enum")) { + const ep = propsOf<"Enum">(en); + schemas[pascalCase(en.name)] = { type: "string", enum: (ep.Values ?? []).map((v) => v.Value ?? v.Key) }; + } + // (Models: emit as object schemas the same way if referenced; DTOs cover the API surface for v1.) +``` + +Then change the `return` `components` to: `components: { securitySchemes: {...}, schemas: schemas as OpenAPIObject["components"]["schemas"] }`. + +- [ ] **Step 4: Run, verify pass** → PASS. + `pnpm build`. +- [ ] **Step 5: Commit** — `git commit -m "feat(openapi): component schemas from DTOs/Enums"` + +--- + +### Task 3: Persistence + apiDoc service (baseline + cache + fallback) + +**Files:** +- Modify: `solarch-backend/src/projects/projects.repository.ts` +- Modify: `solarch-backend/src/codegen/codegen.service.ts` +- Test: `solarch-backend/src/codegen/api-doc.spec.ts` + +**Interfaces:** Produces `ProjectsRepository.getOpenApiDoc(id)` / `setOpenApiDoc(id, key, doc)` (mirror `getSimpleSketchModel`/`setSimpleSketchModel`). Produces `CodegenService.apiDoc(projectId, stage?: "baseline"|"full", force?): Promise<{ doc: OpenAPIObject; source: "ai"|"deterministic"; aiConfigured: boolean }>`. + +- [ ] **Step 1: Add persistence (mirror getSimpleSketchModel)** in projects.repository.ts: + +```typescript +async getOpenApiDoc(id: string): Promise<{ key: string; doc: T } | null> { + const result = await this.neo4j.run( + `MATCH (p:Project {id: $id}) RETURN p.openApiKey AS key, p.openApiJson AS json`, { id }); + if (result.records.length === 0) return null; + const key = result.records[0].get("key"); const json = result.records[0].get("json"); + if (key == null || json == null) return null; + try { return { key: String(key), doc: JSON.parse(String(json)) as T }; } catch { return null; } +} +async setOpenApiDoc(id: string, key: string, doc: unknown): Promise { + await this.neo4j.run(`MATCH (p:Project {id: $id}) SET p.openApiKey = $key, p.openApiJson = $json`, + { id, key, json: JSON.stringify(doc) }); +} +``` + +- [ ] **Step 2: Failing test** for `apiDoc` baseline (no AI): assert it returns a deterministic doc with paths, `source: "deterministic"`, when AI is off. (Mirror simpleSketchModel test shape; load the controller fixture, call `service.apiDoc(projectId, "baseline")`, stub repos.) Run → FAIL. + +```typescript +// api-doc.spec.ts — uses the same fixture + a CodegenService with stubbed repos returning the fixture nodes/edges; assert apiDoc("baseline") yields doc.paths non-empty + source "deterministic". +``` + +- [ ] **Step 3: Implement `apiDoc` in codegen.service.ts** (mirror `simpleSketchModel`): + +```typescript +async apiDoc(projectId: string, stage?: "baseline" | "full", force = false): Promise<{ doc: OpenAPIObject; source: "ai" | "deterministic"; aiConfigured: boolean }> { + if (!(await this.projects.exists(projectId))) throw new NotFoundException({ code: "ERR_PROJECT_NOT_FOUND", message: `Project '${projectId}' not found.` }); + const aiConfigured = isGenerationConfigured(); + const [storedNodes, storedEdges] = await Promise.all([this.nodes.list(projectId), this.edges.list(projectId)]); + const redacted = storedNodes.map((n) => ({ ...n, properties: redactNodeSecrets(n.type, n.properties) })); + const baseline = projectOpenApi(buildCodeGraph(redacted, storedEdges)); + if (stage === "baseline") return { doc: baseline, source: "deterministic", aiConfigured }; + const key = hashStr(JSON.stringify(baseline)); + if (!force) { const stored = await this.projects.getOpenApiDoc(projectId); if (stored && stored.key === key) return { doc: stored.doc, source: "ai", aiConfigured }; } + if (aiConfigured) { + try { const doc = await aiDocumentizeOpenApi(baseline); await this.projects.setOpenApiDoc(projectId, key, doc); return { doc, source: "ai", aiConfigured }; } + catch (e) { this.logger.warn(`API documentize failed for ${projectId}: ${(e as Error)?.message ?? e}`); } + } + return { doc: baseline, source: "deterministic", aiConfigured }; +} +``` + +Add imports: `projectOpenApi` from `./openapi.emitter`, `type { OpenAPIObject } from "@nestjs/swagger"`. (`aiDocumentizeOpenApi` lands in Task 4 — temporarily make the AI branch a no-op `const doc = baseline` to compile/pass this task, replaced in Task 4.) + +- [ ] **Step 4: Run, verify pass** + `pnpm build`. — [ ] **Step 5: Commit** `feat(openapi): persisted apiDoc service (baseline+cache+fallback)`. + +--- + +### Task 4: AI Documentize enrichment agent + +**Files:** Modify `codegen.service.ts` + `api-doc.spec.ts`. + +**Interfaces:** Produces `aiDocumentizeOpenApi(doc: OpenAPIObject): Promise` — tool-calling, mirrors `aiEnrichSketchModel`. Tools (Zod): `describeOperation({ operationId, summary, description })`, `exampleResponse({ operationId, status, exampleJson })`, `describeSchema({ schema, description })`, `describeField({ schema, field, description })`. Tools only mutate EXISTING operationIds/schema names (grounded; unknown → error ToolMessage). Cap MAX_TURNS=12. + +- [ ] **Step 1: Failing test** — with a stub LLM (or assert the grounding guard): calling `aiDocumentizeOpenApi` with a stubbed tool-call referencing an unknown operationId returns the doc unchanged + the agent never adds paths. Simplest deterministic assertion: a unit test of the apply-helpers (an exported `applyDescribeOperation(doc, args)` that sets summary only when operationId exists). Test that. Run → FAIL. +- [ ] **Step 2: Implement** the enrichment, mirroring `aiEnrichSketchModel` (getGenerationChat({toolCalling:true}) → bindTools → while loop → ToolMessage). Build an inventory of `{ operationId, method, path, summary }[]` + `{ schema, fields[] }[]` and feed it; the agent calls the describe/example tools; apply to a cloned doc; return. Grounding: each tool looks up the operationId/schema; missing → `{ ok:false }`. Then replace the Task-3 no-op (`const doc = baseline`) with `const doc = await aiDocumentizeOpenApi(baseline)`. +- [ ] **Step 3: Run, verify pass** + `pnpm build`. — [ ] **Step 4: Commit** `feat(openapi): AI Documentize enrichment (grounded tool-calling)`. + +--- + +### Task 5: Controller endpoints (openapi.json + documentize) + +**Files:** Modify `solarch-backend/src/codegen/codegen.controller.ts`. + +**Interfaces:** `GET projects/:projectId/codegen/openapi.json?stage=baseline|full` → `ok({ doc, source, aiConfigured })`. `POST projects/:projectId/codegen/openapi/documentize` → `ok(await service.apiDoc(projectId, "full", true))`. Free, no billing gate (mirror `simpleSketchModel` endpoints). Reuse `ProjectAccessGuard`. + +- [ ] **Step 1: Add the two routes** mirroring the existing `@Get("simple-sketch-model")` + `@Post("simple-sketch-model/regenerate")` shape (same controller). +- [ ] **Step 2: Build** `pnpm build` → OK (routes registered). +- [ ] **Step 3: Commit** `feat(openapi): GET openapi.json + POST documentize endpoints`. + +--- + +### Task 6: Codegen — controller swagger decorators + +**Files:** Modify `solarch-backend/src/codegen/emitters/nestjs/controller.emitter.ts` + add a spec asserting output. + +**Interfaces:** The generated controller now carries `@ApiTags()` on the class, and per endpoint `@ApiOperation({ summary })`, `@ApiResponse({ status, type })`, `@ApiBearerAuth()` when RequiresAuth. Descriptions/examples come from the enriched doc when available (passed via `ctx`), else deterministic summary. + +- [ ] **Step 1: Failing test** — emit the Users controller fixture, assert the content includes `@ApiTags("UsersController")`, `@ApiOperation(`, `@ApiResponse(`, and `import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from "@nestjs/swagger"`. Run → FAIL. +- [ ] **Step 2: Implement** — add `imports.add("ApiTags", "@nestjs/swagger")` etc.; push `@ApiTags(${JSON.stringify(node.name)})` before `@Controller(...)`; in `buildEndpoint` add `@ApiOperation({ summary: ... })`, `@ApiResponse({ status, type: })` per StatusCode, `@ApiBearerAuth()` when `ep.RequiresAuth`. (Anchor: decorators are assembled around lines 159-197; class decorator at line ~108.) +- [ ] **Step 3: Run, verify pass** + `pnpm build`. — [ ] **Step 4: Commit** `feat(codegen): emit @nestjs/swagger decorators on controllers`. + +--- + +### Task 7: Codegen — DTO @ApiProperty + +**Files:** Modify `dto.emitter.ts` + spec. + +**Interfaces:** Each generated DTO field carries `@ApiProperty({ required, type/enum/isArray, description, example })`. + +- [ ] **Step 1: Failing test** — emit CreateUserDto fixture; assert content has `@ApiProperty(` per field + `import { ApiProperty } from "@nestjs/swagger"`. Run → FAIL. +- [ ] **Step 2: Implement** — `imports.add("ApiProperty", "@nestjs/swagger")`; before each property line (anchor: the property-line assembly ~line 137-144) push `@ApiProperty({ required: , description?, enum?: , isArray?: })`. Map type sensibly (enum → `enum: EnumName`; nested → `type: () => NestedClass`). +- [ ] **Step 3: Run, verify pass** + build. — [ ] **Step 4: Commit** `feat(codegen): emit @ApiProperty on DTO fields`. + +--- + +### Task 8: Codegen — main.ts Swagger + Scalar + dev-CORS + version bump + +**Files:** Modify `scaffold.emitter.ts` (MAIN_TS), `src/codegen/codegen.version.ts` (bump). Spec asserting main.ts content. + +**Interfaces:** Generated `main.ts` builds an OpenAPI document via `SwaggerModule.createDocument` and serves Scalar at `/docs` via `apiReference` (`@scalar/nestjs-api-reference`); dev-CORS allows the docs origin (gated by a `DOCS_CORS_ORIGIN`/dev setting — does NOT loosen prod CORS). `CODEGEN_VERSION` incremented. + +- [ ] **Step 1: Failing test** — render the scaffold main.ts; assert it contains `SwaggerModule.createDocument`, `apiReference`, `@scalar/nestjs-api-reference`, and `/docs`. Run → FAIL. +- [ ] **Step 2: Implement** — in `MAIN_TS` (anchor lines 1150-1194): after `app.useLogger(...)` add the swagger imports + document build + `app.use("/docs", apiReference({ content: document }))` (per @scalar/nestjs-api-reference API); ensure the generated package.json template lists `@nestjs/swagger` + `@scalar/nestjs-api-reference` (check the deps template in scaffold). Add a dev-CORS allowance for the docs origin behind a config flag. Bump `CODEGEN_VERSION`. +- [ ] **Step 3: Run, verify pass** + `pnpm build` + the existing codegen output-builds tests stay green (`pnpm vitest run src/codegen`). — [ ] **Step 4: Commit** `feat(codegen): self-documenting app — SwaggerModule + Scalar /docs`. + +--- + +### Task 9: Frontend — deps + "api" view + 3-segment ViewSwitch + +**Files:** `solarch-frontend/package.json` (add `@scalar/api-reference`), `src/state/workspace-view.ts`, `src/components/ViewSwitch.tsx`. + +**Interfaces:** `view: "canvas" | "code" | "api"`; `setView` accepts `"api"`; ViewSwitch renders 3 equal segments with the sliding chip at `translateX(0|100%|200%)` and widths `w-[calc(33.333%-2px)]`. + +- [ ] **Step 1: Add dep** — `cd solarch-frontend && export PATH=... && pnpm add @scalar/api-reference`. +- [ ] **Step 2: Extend store** — `workspace-view.ts`: widen `view` union to include `"api"`; keep `setView` typed to the union. +- [ ] **Step 3: ViewSwitch → 3 segments** — change the chip to 3 positions (`isCanvas|isCode|isApi`), add the 3rd `` (icon e.g. `Zap`/`Plug` from lucide, no emoji), keep keyboard arrows cycling 3 tabs, keep the no-slop styling. Update width math to thirds. +- [ ] **Step 4: Build** `pnpm build` → OK. — [ ] **Step 5: Commit** `feat(api-ui): 3rd "API" workspace surface in ViewSwitch`. + +--- + +### Task 10: Frontend — ApiDocsPanel (Scalar + Documentize + Server URL) + mount + +**Files:** Create `src/api/openapi.ts`, `src/features/api/ApiDocsPanel.tsx`; modify `src/features/canvas/ProjectPage.tsx`. + +**Interfaces:** `useOpenApi(projectId, stage?: "baseline")` (query `["openapi", projectId, stage??"full"]`, GET `/openapi.json`) + `useDocumentize(projectId)` (POST `/openapi/documentize`, `setQueryData(["openapi",id,"full"])`). `ApiDocsPanel({ projectId, active })` renders Scalar against the doc; "AI Documentize" button triggers documentize; "Server URL" input sets the Scalar test target (default `http://localhost:3000`). + +- [ ] **Step 1: Hooks** — `api/openapi.ts` mirroring `useSimpleSketchModel`/`useRegenerateSketchModel` (staleTime 0, refetchOnMount: "always"; Bearer/guest headers like the other hooks). The doc fetch returns `{ doc, source, aiConfigured }`. +- [ ] **Step 2: ApiDocsPanel** — render `@scalar/api-reference` (its React component `ApiReferenceReact` or web-component ` + + + + + + Solarch + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..f7f95eb --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,74 @@ +{ + "name": "@solarch/web", + "private": true, + "version": "0.1.0", + "type": "module", + "engines": { + "node": ">=20.19.0" + }, + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@clerk/clerk-react": "^5.61.3", + "@dagrejs/dagre": "^3.0.0", + "@excalidraw/mermaid-to-excalidraw": "^2.2.2", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@posthog/react": "^1.10.1", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tanstack/react-query": "^5.100.12", + "@zxcvbn-ts/core": "^3.0.4", + "@zxcvbn-ts/language-common": "^3.0.4", + "@zxcvbn-ts/language-en": "^3.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "elkjs": "^0.11.1", + "jszip": "^3.10.1", + "lucide-react": "^1.16.0", + "openapi-fetch": "^0.17.0", + "posthog-js": "^1.386.0", + "prism-react-renderer": "^2.4.1", + "react": "^19.2.6", + "react-dom": "^19.2.6", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.15.1", + "remark-gfm": "^4.0.1", + "roughjs": "^4.6.6", + "sonner": "^2.0.7", + "tailwind-merge": "^3.6.0", + "vaul": "^1.1.2", + "zustand": "^5.0.13" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^24.12.3", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", + "eslint": "^10.3.0", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "openapi-typescript": "^7.13.0", + "postcss": "^8.5.15", + "tailwindcss": "^3.4.19", + "typescript": "~6.0.2", + "typescript-eslint": "^8.59.2", + "vite": "^8.0.12" + } +} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg new file mode 100644 index 0000000..6c01309 --- /dev/null +++ b/apps/web/public/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/fonts/Excalifont.woff2 b/apps/web/public/fonts/Excalifont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..98b9616a39025e37dcaacb5bce5a7791666bf38b GIT binary patch literal 24956 zcmV(}K+wN;Pew8T0RR910AYLp4gdfE0P*|)0AV2j0RR9100000000000000000000 z0000Qa2u@}9D_;*U;u&)5eN!{o_K27zZF54!K1K;#QJk z{zs(Z3^giB853La|LK5}F@8U=(iWhzWt?KAv9-$+MVnVK=-Wzc4v?%fM$bY9&YfiT z_FoSzw5R@*U18*on=$Cmz_yV;$;mrBKeyihfB!F=5la~K7$xPJjoijYhaf_W2`Gpd zhhX%9QKz0@9L9MVm1oqIXT_cJ|NI|5x$k*@tkmVRh6Z@7gT!kWs~Ex%HH|=G>Hq)c z%88x4b4#2oARO_B1GTRjg{(A{EG*DKZQh;BS(y-A!qv zo^r~|shTUkN*w_fDi!cYAg~N_p~MFF*_o8XIsbnyt(RtY(wSXBY&R#d0|NlbC#Hq1pX4;wdgJ75CXtBHF`id^_UVP-s5m!KQYqx@tWJt0CTXK?v z1qT2B=SrK*E79F$*RrBtI37^7PHtW{lat(;0%ubMwcv;|We~c;Fgrkn+SJHd*SeJz zvO>!K@3h%NNQ^sqUbeZN+hK*2J;M$X<1h*#I~aXoC(O?-s2*ckghe{M;S`#wwbcHX zNVkqaRoEm7jp|e=d=5Rl>oURPz(T}m{JsbI7x^+xRJLEO0#%CYY*8u6u8x35(s2WeszLE=JFk$?4&c0y ztt$=-P8q;g0W21_V}AX|5<&vE^`@C+@W{2IK75(#$|@);N6DGf6UEKikP8t8?e*iphi{~Un62vBjQqJ){>rrRrGJbf2xmH1NO&Qb_2fXT$ zkhH4(*RR);|5;%K@ZIl40^ghq2he`M6-z1*fHUI1Nwc_$svFC}POLk- zk9L&kt1GUB6=WPda4feTdorE!sX+&^oHK}Qc091CHlfE#ZJ8fcUnzf!+YYh1s$Gdv zjgwe4xN3_#61uVpeRhg{^APuNEIVo%6M)oA%}K2`bMbg68@a{}SO%m(MiRus3SGOi zA0#e*uNP=*Q&dci(vAQZZJo6n{1U$DT3J$yb;`^gCE(hW(wJ*g8%Gp(6{VM))xpz3 zDynqV?FFm#`l!0)z3F>@(3Xzc)LC*jgx~iJ_I*U$^*S$4dSdu0ALEi7y18-c`c=RJ z-B>K`oW)Sv?BXNUI)FdEE3=Nb{4k7mN#E=J@MpQ>sAcjL_LW7L8wvmb4G<<^93UiL z7DvVl%9lTqKq^#fQ5baegpA^d#7p2V36B!9N+ppli$Xr&tqeYD-`v+UGx+H?U$9=Q zjWJ*ovn{r>*#V^4{Wa44Vy_JQ9gyvaqjFt$Lp})XIw65)ZGr6?d!Vi7dSk$x8T6l7 zEO*%#lvs%A@QZG$0`P+Y01!bCiv768M84|nBk@5t_KD&6!e&JjRCyMgj2jLG1uk9} zgDa0iV@gz*lScwd?H*I%erUyPBats62_W$*aF@WHZLtMA1PLjnU;;oP$03hA`3hNF z7hs@BbzloJVCzFJ8ufq=n65Y_MiU8%79+6~A*CXiCxGy(R5U{Q5itJ13j{$Bh*Tg^ zgDeCTp`Z#gX~IRI8v}+&Fhzky2R1!KjNmYVD-JyI;7fo|lIg_zV?Lr@lW2j3XcmEKu}QSlDr~Dwt^sRttuy)7 z+ely&iGC50h%66>;9&%Odx5tfhy&z0$dWh&i5oh-$kL{Ibj9k!RDwYgwi|t~k0usK z40gEY&`<6{F+MIitn)j2MEWpBzp1AG;|En za^$Pk6q%qZ1VI^KEwyPTng%lxVcQ5?LDiR925%Kiyw!e0G6=e#K(YfQ z`vE&ZQq4r~0#qsxkT)R9>--pDr5<1vNzcy?2Wiw{f5wVJ_yP^ zt+PLF8jvI{!9IhvxBd3|)N>E(JxETbOYN%=R&+6OAWY;KIT9oc(Ptl0}UDB^AyBfr_0E0JhWL-{)~Rq>0SL(R~B}py?&XoVcyYgv+sB zA2q0_3;-l#SBKsd?2F>8Z9q8y;=QBcjkshmO8l6GH2)7mPxG1|0Y3r&KwmRbJb1J4 zFe673&4BZX5m%p0V4Afy_{or6Mtq0bOYK0%yWQP`LkQxBVi&QySf3;ndy0d^k>age zIVtP=_afB?thdRw=h$Q4?DlaF!~(IaEgvvOCzC(n`$2x?0Q=a>2(zgC?f?JGmnZo( z``*31-JR{NTl39lr?U#qbeQObF$D@0jf1X$Q?~{|KMsO^n9TSKkn-z5IpL%}u#mPP z7mQ?(L*6QIKcu}R@VrCIVLl)25|%W@lOjki98(4%_?adP*SZQnv?k_)_LC5ch4@$p zU`|K^(apa*6nL2cLtxqhZtpLJJh_yl4W42b9iVOE&YD4KrMfhl_pEdescwgd*Cl7R zO}rX}FlRMVWMBxM+7TMZ_WXqRu(W=7wiXSXAGrCx?zX;JTL}m64#uzMa=Z1q&@e-q zgCGw#HYd1)(~IfF3FZ}&RkJy|w=lD^H@C2*mo>BpJO96PoN*f$8h)a0tRPH3?%>X0 zB{x6{`>0uL4bWDew#ElaR)j=^WQ0_Nf(S7lA=34nynGN3@S;ktH%qRF(u;!g!XXn5ZMz5DnQ-4aVo>0-a;r831YUu0~ypDbf`|TuV9@NS4~)x z8%~G9K;vvmP3EL&DqLjI+E7)ebknwx=3l#M*&D=W@P^6UoZlk-=&jtL*P~axZwF&< zVLE`sA5AEa?VLQ<(=RV>*c-L)o5!u{I#;8G?aX*BNZo?A?DLsJ?$~VjRJfoL;E!S> zf5iE)IHi|h>9}cG3^Z1%HKBVy)n@(-tNJ_KrldQtyMI|Qe~yJ*P&SJ4 zk+Zv)JywDfzkO!Bic20hw054BkNr%c3+8scmHtN#?g8cl6Ph-(rEHJKtoF4_86(Cb){O}|*;D+P@k*}vbL&zq zfj0+9uHOVqre$y~d{StIQ`|4_z~%yEYpJk3%kYsp<#tIC!<=QQJ;ko88kL^Eb#=SY z(4kZ2ncEckd!ELtUDkG^A?lb^5>0Im(ez#v7oqnMXqkx%)h5l!9Py=G~gv zyz?J5c1_R|uUMXC)wKn9>FOVRs=+9FY59IUlWcDhRdhg;p3$NdK_r5ns|n^kmfhzkBcL0#uLZ6Dw#h?FA8h zAbIEmM_9QQ_*>YM;%$Ag)IdMcck`9*+u~`ke47l@x9J)wvAfQ*jkn{Q~ugWpyNdu!Fju zff|LLYvJzXfT3d9J@1aV7}dn>5^Aq7Xf6Ah5K|Yqp1dZJiWusOr4q0T`=FCNbbH+6 zeKNu$Wb$})I{VQW@#Is>OE+|t3Il;+krx-2S}4oHNxYfu=6ZQt89?}`Tp+jUP9Pq( z+;p6pfa`+hGPij*DM0&h{nZRzqT+(WZ?I@f$3|IkXZ4zP%^c`2|JLo)%t;TTMEh)QT1&E=Gd{{h5T-ORdx6_q zaX)vs(J0M(PYg7wr>OXkMZL&ieqtsrdslFxRPV+OuVNXoi0eIe`cX*^q0p5JEP|oz z!&*DT2$L{-JWsf4^M>pNq*3vl=s|Ul$mIY?{5y564{Jr0_!LS&dIW1GngciIyy$)p zC-6uPH9S;j3K9zOP@(Y2?=||Ib5J1_0O5>kcy<=RKD9@fMHavK)!y=wCtfl&PHEk5 zj9@T2KjjiuM}@Fv;nV0QfdQE*DlG$2_^WAf1kPs7B&}Nw|Ag6;dI6*8#4JiaKyMS%o}BN^cHtO4Ac!L270 zF;59KOeavx5@V$92J8dErMTgyY_r;yw%U+t*Uv-YT|m07_!+G)_49sily_oCt#14C zs4t^lKP|j9cn;})G63xm)t*gX^zy@TwaT#seT5R>%+Dh~*WFhx+~FL#(um^_-Lz+6 zH4lPbPk{#T5=xLpNVhFx`x&hIx)!RKbLSyR0sJ;fv!Jt?oPG0z&P<4_5sd;HGHj7v zxxhNPaosr8Y`bbxHhZh?uF4W;m5~WP7L~9mQ``dGIf>kf_1ez;7pk(?>@ge8PLcgn zJ^~!ufdgYPsd!+|@eWN%!_5afrJk@#C+cFC2!c5|+it3TK1_rxkG zqreb`>GF8^=vlnV9k7Lk1Rr2iJXB9x_msL9(s+0>Jvg+E#l;i$>)a-GO34v8f#m;vjGvv6;)J?{DY_WnsL1)9j9A`DYl z%!yRe@u&?MC6^)U0p4G=d^&}Tq4@}KB{_81A2ENAdAPVmJ#~rE4H712fp9Eb9 zszB7~%wb^9`PR1@T2(pTs6g&i@lj{$eR+~H0i@l>au7s=aTacjCe~FJT7w{_^Y3Pu z>ve*+N#e60A*K@MuDDPk&=q1Jy3i^(c)-RHhWes-u@^_~3;ShSG*FoK!3XWo6#h zt67(Nk;;Juhc|!VOf_-E6;oa}u7+mCs~%N;E-vJ|8Np;fISTRgO^S2rPM`ce*?-g_Lr!|Xf%{cPc2*BD1? zoJilwm`{?;=_U^UV}-8l%V@DJV>qtj7R4n|O!Q*TQ(UAo04))daHL9r>A91WB}#RY zgE5owQ%0mvp*0rv;wYHG+A{-U4JFq}{&;L}V7!QY9e@a1KS#_GSNLmV@^D7TLz8r^ zQ1mAheO#t3KnvPZk)OFut-Eg8xMeQ6qX+B~`T-#;rX~`cDhH~{LE5JpRKHkwn1vhM zY0Yn?#}_b!7-(-HY3I0R%y5*Bl0qH=ZBZ?RdWt9o$UaKThGb3;vqofBUNg;?%cp6z zsYrLf?oJJ&!<7>%sZ4t@pR|kQz7CZQjuYpu{%Y=e!@ai1@R0cF<_X`%G-75o4B_GF z0rYz(soWem{bwIPqH-CXU3p?9!z_G_He@7W^;rIKt&#uY9B&Y}6*Le^zE|U?cFc+Lu&6qyl~7<^ zbi3OuorcE=scnXKA%>Jig;-rU`|xmUm&dETa#ldQpB60x*w1e|n)5Ul)2OCW0!$!w zy2_XzitJ(f6HF5dSCV3@TmxC}mvc$-gr7o(F3#9Tj!%U#hVtNNdZwb$#TR!|;jN>1 z`uk)$T7AV+_9t*6A^d`R`{&-RTwS|k17_Bl>!u9S-4za@Twd}S$#Ij5%D^)WkK2dI zhfC+=yfT5(trq0jkEP)Q~a)%aanH>MpaSLcuN|Z6Y zwHu_=Oa61ky4HtVaNQuBrTcjfn8zbGR0vWhX0ljWg!$`{TY9lS!U=>v(UmrD@{g8F zO?w<4D&(Te`DG*iow;`)ei+1RsItJ1Qq7l;<%bHfE*X1m2Tq#zZ)3j$^^#rDdY26t zN_BLRqDuNo6w&5M>98E=G0SQb;U-G8Q0qv=Z}w5}Vn8wl4!u6?+vx*TTXSiJRTG<0j)ca2OGt0M=nIaB%a!0F-Sc0@a^ed^4(SIP0n? zPgc~8_mAHcEma=@>1=Z|-lwi5ok>&+_Z3pM#?N?MX60ka6+HB|0KP}+BQl7_foe-q zmSy0$Sksf3aaRF)qgTm2=cH&C&e&S;xX2pSK08yKC4Zw%c*SK14UMRe#lID59 zH!%6n8a@#1sT1?SBPAhxY)7ldUa5Vc(NMbO zKtZI?L8+(d+Jb(s!*`C^M8p?~M0Pu$>x=S8>?23*i-+z6`lCk?8CIxzrBf6(y@b+Q zP{yz|#rrpc+e2TtegK;WCga5=Jx6tdp_W+LLzg$l9D906X3)x2_%!ojyg2EzcJMqWYC9*P4^Arl(Hz%NzTU#%S z=~xZ>Mh%wS-;&+~__aa#DY^O+XESx>X@~O=ZCOEjQi(3Q33$OoyUIXYRiv$UwLTV5 zLHZA-iuEGFT+7oT>xUeI<6v=LDDu|bFKW?)>iHco*3EnehbL)sK_!SeurscFY4CF7 z#uYL?_LbA%tYU>or8dQJbwe)`>rv&E8t*pexWe>#tHJFRC5}F=6P0w0>n927y&Ucd z!a7$67kDf|U1d_MD_lMf=H~|og(ePibfg?hm>|+Ep)0YIKGd+Pz;COL|F7Sw7klgk zwiX^1xum^y5e&8(J>TOw^7Bwbtu~u;ARF~mAPaCcx?HztNI`TyMagOH$ie^CvOWyM z?{L3L8mV@Fr1iI~$Pa|6ZywqEFzwS|J4K8*k{9fd7BoTbe$m+Ew)ABk-`!qRv7o(1 zC!esc_^z*V-Yre%4xg>Ka@&R8P%B+ta#i$tU%z~1sDuHfZ8~}4QfWM?TG)HD@--|W zn*!>V9P_Q#J@Mzxc~E*Vpl7@P&Wa8m!7~Z(L=_n~{CuE+mCAFEVLvY;7LpGtpMTbB z`DF;2^PbS^E)6arZ9g;B+Wh;A=0z5U`dQk}-l-e%UL1ek&Y^G!u9FZlcAfYzroEyLzmJ)V$Uy=p5B*= zrHoQZqdB<1C97SQcESTJ*P8+i*y>-4ydj0^GmWmq0hP z_E~?@HcG#9JsaKyTVZ5_8V(++O0GqsT6T3r=Nf4%pV+mA*4AFzEd)&fX|HlXPFp{^ zss+X-x1}0teSTUg8Az`7=SEnx<~4zo%>k9*>}TATZ>U_x-;>5U-pa{J*Ftw}2S?Dt zYdl!V@bPR@alBp*GA63x*zNNr;XrflQVades>#7=1y;Rnk{0X60H%Au#VAdsBEQ>k zE?DD#86Wz0DiK-rRN4we66Cbk=2e7kfBMxQiJ+SnF0tst(0$9Ni%{>CQPe&$os!jS zpnLlHoXWVLs!iw))Glf6QG!bQ3ao_;RG~@7DP4eN$0X*nA}Y_FkGQT>y>4hrF}u2g zCWqrf(mR1+^*;f>9dRC3V&fV=EYH?8sfO0I3lH+vbHO?U&0uz9ux;iZRbh4Bow(p%QHpxkQZ=(injGLJswILT=J>yfRa7 z4m>Yrh#X4{T<-2gCnmM)$Q95neH5w_8s0$R<&B+=NbM+q1Fv=YC+vGk+p#@7>-Qls z21Bnr^~`j)JUqWwWEuO@IH51!1!2?GfPRvkX^>X8iUJl%ICVTd=)ZnS0`1-| z2>t~#x*GN!yAgX|E1UZal_C`}B#uIHb1KvLC#1n`NUR74x5#w;BlcjoTpVXP>b@*f zNTEIiRuDox64sDGJsPe#fcCqS!pz_>cW}I%-+Rxka_H!bG&ulAmg{XUquuLnmEXE| zOy-<5LId4QH0P@|0@-V(K7oK3R%VZYEY_BdLaQ(xFs>sq{*1Rf+BO^qb!|kvxfVST zRp*i`jaVwN!3U5ZH3&Cxc29$CAGo^F@c3LpWzY&{b-kX#YCuGWD%O653ZXdJ)4l3m zoReM~x4eXp0J{d))$of;?R<%R?;c)U#g0-)KImO5L

o;Oy<{h380A%{&LqaaMzpV}=F?c#b0M?wLPUnB8~?RnOe1x!@Q@!-w;5-iQ# z_*%Z_XOq=n%Ac70^pGc~KU+w<}I}OW_r>0anu*2@ib|)(M2W-2Ssm`3QXW9kE2M#AhE9Q}>q|DFO!9ZK! zXm*?|2U$ceQTiNRl}^?0Qo(M5$Y|Qy#G-wBtkt2Ksp|9-wmPBCOQ4f0K8bTLdWP}P z_wwY&*sBPYOs8d8)9{TwB~|(IAJr~JmpH6X22wT4xsJ&N?(%zM)mHvDK9c|)i{OCz z6@M~VzvWSmzW>;zJoX#s-prp^wX+=wcauaOXnY_D(PSLge2iMg7*x$D#l|(wB)b+g z(N6J^(>)LpZJu%>``et~>ZXuF#HQF}iWul<_}8BD_zB=b zMipvg;=~A-yhmWq8Dw1nTLLQJbDY_W1%u#AK)dq#3;_)hq>YV*r--zO>ve)Dy~tM$ zM)KcguG#a4$$$2TPKgZKn|O3Ozg=aJ?oN4lSCe}RXceePz*w2iD3cGYykW00_LC0b ziJbjn*q@z32Yx*^=gkD>Lkr?vi%@E1^^;LOayZ5E+kM8YeBb{8* zd`7AKL5VYhOHndVK1*bOx+Z_pt;F`bfZ~A40|`9Q42B3@#fyxtU+lU$UrHE*8=TWR z6yR+*L)(^3`+C_usa=RdJ-F3>isp(HX{db}C1=|Kcmt((zq|e2=f%FH<|tb9Nm>j2 z-FHV=k?H>ah>PGvkaDuqziTQ&(-!ovW&yMH1(Dbj+^}UcHnC_Hlqyt!&KPd3&C^|f zuM-N@6e1N?<7lptZDejmk&L|7rWS^&+N3$R$yxT|;+AA09lZSC+$O1v`A}?*ftv~{ zrUEwL5oU33R*9oyC6<3a*_K<0l!vTn)PE* z&GOapgR)3Hh}H21DSSwD3Rhy{h%uONW_IwM1=6(1Hps=7xsuz+ijASlCUn{P9!H26_KT_brur|>e2V;GF$QaC7B|*?)4VY4s4ky;Y5}0O?gX@ z3OpKB=N5u@l)88B4!`}`>)g0+9etTP9-&Yk#7xHF+4w8FG`~M*r>shctLii$3Y2ty z3b{h_92pZ)S)|dIj|5y{f(#^elP9v#drH;t22MSa4m|Zc=*FI6$H+y@e0Ln)eoV{w~yF!E3PvT+$EibIsf* zyi17@^F79yvWqSFf&QO47q>^SXI^xtqrTaZJ#D?)A2@urEOnbm2_eEG0z5j_cpwx+9@h4Yz@EjZb`hseBdTn=62HGF~XSB zRO|gRsd~#-cje6>dC9tcLDMU9T1#=Ie~I6kd>{A+KcIdqRc=6`{o}A?Wp4Z3(yxu5 z_*V+Q5P}!H$CA>Cj0KtoM};D~_%*y`1uYjb32vV^pE-Id6dT%Ok?EI~a!(&oUMtn>m}(4knzyzsJ}FD6R*RuO9bslV}jHW3KQ>t4r(o z)$ku+GJfAH1XxWaG~u>7*UeBZDHbj!X1E1z9deX|Pnb%=R#~T)z+U;iKXY(fuDW5T zI*#?pm!>Vz2D=Xb=PF8AvXE+VSD|~*CUT}v2Yx%*F2-&iHJmxG7fUxYwZW*B*Eg{v z%`q7Sae+%TzD%lC(xgtLi%Q~X2J6qKY6GnjPzYkIZqKp>iM9xS=RjxQP+bhCDNj=&S!$p*wS_Zpe z4U`jJF;fE=pzfU!N8iLBbg7&+WL3}UcDy`V2+|~^>`^S0ctIN z=GmV@&d)yr{5R}8D9GJ93-@E*0@%d><{r#+3uT(?-}G-M+df^Ntw{HYY%=v!ACAbn z7!*tZnCpim%RSk++~{sHiJ$G)GC6#yj?C0FI*gElfZ7$-@22q^5}WUgpI54jb~$oA z5-x&G-h(dEz`&D>cP?y;=)vdV9e2JS0RkrbFUSCmy2EK+WX!jGo*}?@MgyT~;Fr(s zv3{w;sl{JP`cYD7P#^vt-iAvdBgZVQI53Z}_+>hGTbOANys&p|H&{65Kkd9{uSA0z zR-Nk0kwH920jb4yttMo@BCxPN>T;TR%!A#39Q#n3Ikhl)pU=OyUuh#+YS0CXv)$Rx z=)pC;xPFvze14yaQ%p^S>utavMvSH5uf5mt{%I7Acb?TsW;0751{t2lAP{Ma~~a=8?4zsHZd0+ zq*I(bihCPi2!a?G68I6B;Ny z^~HE9@Q;MOp;%zpgRHwgrevUy`ThTj--I<+XmzQnu7ot%#9v<-<3CMo;R{b(aYoa% zpK$^oxR^{y!r|b&p&Lxfk~PUBv#z4*W2exb%K1wv9kE$ZF9xoZir(^yrfX{%@kC)! zTPO{56*;cF5Lf*;V+wa)3F`gaNDjpvQ2#z$ar9% z_&5;y>B?TZwI9!CNCuDixYFsF?d8})MN&r0*i?q6TwzrPOee9tC||Pn8F0bwPS}AQ z+I*;S#O0;qN}m+>%9cm48&71!=~!MLlq3mBhQ%0d)8WPXSfbl}(iUD?k@nKP+2+s3yk zp|^?DJLV8Oe9b8hPJ3LtN{7jti;G|ec}Z?xTUl?*vN^70B@xb~+cy1k=~vwau;Xe& z1KDF2g>>p?Y3HdA*Q1nY9D7N*1wBMO!oC1|A)tp+BodDfSdHV*HsuAfnmfz z3i;z$`Ub!7tQt;0iCfj+cCvOb$(|^ z@9Vj<53l;f#Zr&;owddL4otobglg*UzZxx+ORl8FPH3T_o41TZraZ=i_mz}vWNebL ztyl8WKQs-#TkUVuf9B*s9ngf@9i06DUrCp_V-N%vxh$i>tOZAADIhYm`B{)UiD^Lz z4)*d>>#_G(tytG^V^>K~07wt`EYaT$%B)=l^gyRcG+=lms*yC+CZ7H+nn#@5iX5hm zl9749gsUm>;9eymEW(2YNA>=w8osYhfULpw)YdF^oX7iW2WFUI$*?S0CZ@?b7QU&d)|i9@8s6+*`H2v<%CGCBTZJKS#%I%*e=oy9dZWk-G) z+ejpv;agx46y2%3G@^5^=2m9+>9Vx0UKWNV1F#>5|RA?yI8jfJvsI<}=I-jkSm01Qw# z-<@n7HZKOBNzfDUna+ImBUEGXB<^9vUE=<+X;=T@?OiSdim=5?mYzq)hM!oYQVdPN znfUAUyj}m7;>&Bxy#0_oR|9c+xBU=8%_Kak-{z;X*c7p7S?HC4=ctf zX~NKC#6iFMB6iZo-ud)P1*bCCwvD%eCdQgX4wg$f^fey)ZM`JE*^U=q-boj%=TAaZkZQN(zAc~$c`^Y&<)9IG=tP6KWid!2$<$#!`KQrw*#&46<8-Sq&cw}f#gic4v!-2b&LUsi= z7$24L z7dremG4~qjb>^ouf`QaXW9~`{A%yrAPEX_lMq2IAYQI5;>SFH$f#tsaTDfBQ6~;zf zf)R$Efj|bRL;P1QL4)Dolh$e~6Y4u0x&^Q7DZE|JL4%MWlw0Jn82Er0i_$rPIr29e z?Hgxls(XNeQG57BNo;Vjc6`)aj<{Q^AmNn+l(7|dFpZG75K6=qtJbU*;eE`q6(LL} z3u2gI9ov8OBC(D&Q>h^18u=Wwl})gKRi4(vH#Xgyc%Hdin-Z<@>c(#Be!Oh|(G3=D zh1Zdmp^4B~L-P!4_gKCo+q`}D%aHb$fyT|kgf#q%!`5*ePeH?sf3`oC%Fp2R8EL5r zUm3lUEq*_8o3AWjfKLo!7j3ANu)vH9_{S0?&kCK$jaT{R`4O;u=#M4-WcFHW{D!|X z-g)P`vWb~Bf666L7M4C>pJSg2n4+?2M5HPJoHk#2d?w!dF=7k5aG5_otr6h8kv0S0 zm|xS7A=QQjMnXh-n#dYGC^dn7l2gd_-~w*@;?}|B1X(J9Ysk^F`wu=QK6<$)){{xY9KJwfv*bxk%vR$qCld*XxcwLV+C38M3@lR zKg;*1hd#c_V*NM+ii;27Hd1~l_C-PP2MTSeh>B+sVE1dwj}^!SvZWP(mUPc2A{;sY@i8=J5I21DJf2IgbU`y;a&-6u~4B_>3GUjY5pH%?rk4Ukc4|170uio--hEc#v^FG~zOW_7 zzAcelj^K(fexnX5wggV~x$76AWwBpJW>APVxb-la5Goe4xo?r53Q0|bpW(RukU`3! zH#oKb;=BvhyUP+#tIUeF%d-5PaiO05Vx{nMG8P1h%i2(e zx3Lrm;RZNax^YXkj1lnAbBPMt^3GEO#S{PRU6N{4ImdfVj;7yOwWH1&1WiUp9?WTr z1ND7hbcYrdb!o_GZx@^cS=kjq+oy!=PI4t-hgKwbH1vj!P43KluZ&ZXgNVv10w~RR_Fe`L&l{?yL~D0UXses!E4acPGq2#f!31D(8G9~FJ_W>5TIp@ z+QC}NdI}rWJl3Fp;?u!J0*SbM4I-E>-Oo4%0x;LZ$pY8k*jWWw&v-;p>xp=WseAq% z{m`u9>OS2g;z?GI_%21wA7{L!FZwBAH51Fq?tA~RRyH-B52yIBneZsG)Eief+gnI; z1{C-ye`uQbzI--KKitgU4P0GHX6?32oDFbB{Uv?gj|&8>IcE;anB|E<&7@Z^#>RbmepH^a zlnO&?2m_D)=X0rzp#v8_h(ehW8qp?d{jB1U0wkXRzG4&m=9%xp(IL^-cEHBDT#@4X$$0-=A9Sz zb|y_=3#wUuL3&d71=*FRuKG^>`H19v#E%yLZ5nxTJsW-*!kaK_w_2{u_3)6fz};x_ z_(}$wo6Q5|7nw|%9&B*md_4YQI5$DTV;bpXgjBdhNF`bw-UsWB(K-#$!8)y1^7lSf zZ|F~xPMz4irHm7RH0{r4SM0e0ezPF0u62E%yBUUU(pu(O`=j~Pcxp~eZN~mzolVf8 zexh@=bgBVOA+p{}@uY<+j6J%exSu8_)si07w~g(!Z8*n7F=|2SxK~SRQ=>b}`d2kf zbF_PQb+*>XzUUlGJ^o0c5k|@53x__TZ(dV9uKuyWyK@yE%sx>(+Y{&gSRK@UGW}(l zcd*Mx0AOrx>)*Jj$`w z%KuYyVo5%gp^)cM=+Xz(sb`Po?`I_%SRt2yh`>_=kssV=Zcl&#-;Zgnh0#dLqV`YGvtzb)9` z_wfkuucEXv`DfKo5b8;mJh=2$bGe;nM$v>)_qA|1Ba)I5&+sNY(P;QRY`Iv8y-lK? zL_NWTi=G?)F4XA>f!zg!3Q_Kwk?WP9EVwkd*j9iChq1?6f#m0gV<_%~{sTrAw4~fUA z`!jad;woJLiChxr9Unl&FjLJB9lU(67n&22 zvP)wXCq|LW@5fwT^v8nN%8>oAE^6J|;IB#0a1~k~f)`B81shnZQ@6K&#%wWoV#)OZXqt{bCkz2jMuPqf`yN(vHf@#>Lta1#gN+>uh5^akfl^$5mI~KSkSE zlsJRke&UY4ULoZ?>ay}|DL9?CL6-Gr3)=8rOLBu#pQz{8aXDv#%Ll;_X~Cyi9p00; zEPdOgjhWo(G7m;?`CLl6EfJSrbZg_?sRL+h!@ z1oP$NmMQFySC+*3g120eo;%>l7d8`6LWJvdGX8E7;54&M;aTHXXoSgyeAa$L-$y9| zZg&uq@>n<hoH7n9g3eBkam>eDlP;w9%ZP{bA{{N=RBsD}uS+a-GcYmI4Gv4O9og z2`nTV=F$H1W}^|h1qC}G@clgMG9CAJEWXf`RclxCcoH+mBjZQc3?C?*Wvl}~9D4NG zO-$O*QOIUm?Ta+s!INaI&=_VqbC%%jIzV+3;jQ=#yo=i(p2se5$4|K8Pml&eaG>`% zuzj`GhrG0v^Tpf5_z)Sfn3v3kKDmKp520hzAVPfS7X8Lrv(RySC`1?dmL6*hs@U~0S zj5OVCxY?_)MCvmF!2}G>RbBTpo|yFwsZz2Ct$4Rkq#81j<_GPOqi_mVsmPW=Ys{dX z%|CG;nkt~;N#v6*F!{V+E6>#fo{l1MB&t7kFMg2)LX6^{*HR~)0F%JF0{LqAZ6uGf zAUi*F<=4j<-Xrfh;SKDV)Q2~JUKl@0$b`~=b6ep_X*ulpUeoRIp8^*XRjG!F%vC9? z)QSJ}!*|D~lN(?a2uoDw&jCAOl8{wa&eG<1Za4F#7(t>954E!clIsQy2Rtm~MQ&&k zLS*P)iI0r)rG-QK;cK$A39{Q7$=Y$fRm1LaPL>Th1iAbvt^jv&K9vV8UvZV=d@8qV zl}pFQcaJ@xHfY#-CHz(DZwB}1g~uZF9@ z%KF;c+iAZtz2Zn4382gcvgZ&D(_fup8>#sICifuo|CDJjYhtCh?IEjhM7T?W9~W8D zw=8V&!DtcJVm3LpD?X4D=s!-SO#;h{UL@?!reskYkd$M|{N1p!em_*0I?)5QMNDEo zhdb2w(-ZM*Hb#c=6Np{#t}((`Lc|kr=GMbB-T#mXhl~CSn{X3+VTvWJAN(Nerx`ft zkd(q#OR3P>0si{A*&X>Dl;|?g%%Nw4sR`h^+9(C@RsPzvo#n2E7!h;P!Yh+(j zLy4|O zAOGFye4P=l%X@@p)=8#K1DuFU*^0}16y(XC9CMof@J*uIP|=+S`Z4Sb%4kISNCj_Q zfEQ9*rcm7;(q3Bs6KF5|XkrZUb4~OJQp(RR-%3M*-dcBRXBt~7r^Ml!INL`r-Lq*5 zCWj|k>%`N*Z3>2u)Xsh}y;NX_V<)$Ih}R8wg{n6#5I{z%p)l@aH*;SpTHTAC!#*E0eaLydd$A1f2w$1{)YJr$8FwEo<}cWVby zNCM>6_aGS23#(?$8d?4J@qbc5`oJoe>)BVIrD98m#JJH!I{ra~cA~*y%$*y4PrHFx zBGCtFF+4a3pp`J@NoD#A)oJmv}Xqw{s4rgwW>T}ZrC7i&8IklP}%M^*rH%wWp$DqLkkM@pG4 zVqxa%(zFNt{=Akn0Zx8N#u$Q7Fb%H8IY^7R zgj{rcEPspm`c;hJTaiF^!mvL0rp@YwOUWFX^$ss^)rgfe`lga(BB zx8se^_4(?G@UaTNa9?{VZgR-EhC0zK?y2ADLRz{IEWhwi-@<^@D^X>_>;D-T!-{y_ z)ZL~TbM!Hju@JAS+qM?LM~k05-5o6c#gFf`B6Q?789(F3$unCenBWW1<2&nUQDouv z|Noo&@ZP02eFW~EnyRgJkBR`!Uky#F0{SW+tLL-}E<55^x}v^#Hy3D1HH41)C5_JR z=$nTIx8jVeM_)HMcGhp2o_YjGRxknzC_b9qma)EUB%^Gu-Hk-AWT3*G6t_uCjGD;i z6PBJJ{3-6nyS#ul=MeGT;_83*Hb3e-I`r%y7|&<$ z_6@#b=pjMpj(Faa%-k8`JKTR@_QK%YroddW2 zh*;0@O6_w>b%YFNROFjw6IZ>!y9XMX1&8Zz>f9uEW^K^d;cT?D#ep(kgkrh94%wB? zq)%fYKS6(aWI?mU9d3t5#jl|{Xh#c`Bssj5EQ}647z&s&2)Gu&4RCEyx9ayjVG{(oh%j$tDDJkAA@=PpqKLn zOI^8g&q^NKGl0&hpd8>Cy*8_v;FvcNXh4coo`dELgAGsHVoHOyC|F)27~Zahp;w2W zx6`O3T_=z1_^+j#!&3gzu4L0miQipBkPpX!?XALDU@4lkEnAu-jq2nS&{hvn8-&(z z$5KTL6LqD}b0xVE(V4s|vZrB1u|$VXHF}+$cHwX%MwxwXQeybc$H=4L=QfA98;DcXbJ}U-S$t4hLE}|Tt%0+v_-$7 zLvl~l0juWx#$bnPGK!eL6Y)P$YZ}*#Ig(CBimA<{e@VQ#x7TKjhl}<;zx?!Hgg_k3 zPsbKzU`2D8K<)ig12M~@GK>F-1T0zu)csSoA-LsR=ZkCgulkwp@0tJn6BpMD{`o*= zDv>X*RF9xEzgN+CXc>onIfQNj48T)E8zverBqAp-hr$|edZMkYifNpsEF`tPYJt&5 zA?WQ-fU#xPmUH0|7?HYS?Oz#Tlji_}2o~Xg!azW?SYn5Q8&xt4BNd_JWB;_jcIC-y~k6p33WuYnIN4Z z7;LlR+K6Ce8IAE*0ABZ?znGyC%Xl{@dZgK%$Z;}kB$-y}Cq}PK5E=1p1STTl23TzFik_EXVHm_5SD$X4b&|-&*oNB-1mE69Ef_1Pp_+m72EO|ev;vE zd^wj!iU+rb2W!zNX!^qfovj*8t*B&%GZCmyF4u)Vv;=P49V7;0`1X4W4=+4&n&a9; zh{+7pu?Z=%DZj39?tGmA!R{5fcm!Us6Az802SvZ~<~pm;95?J7yL~DFg_(;MNe5X$ z?2*VaJ|h7}Ub+jo zh1BbpFQvU?!Tx1F{S&kxw9HiV5%K3L*&!;MfJ-0cr29DZ1><1^ z+3(2mC&_)2D1KCWwdQkshF2nucy8INJ6j9Y(HT)%2gppJIW?F#vSaB)XBn27+WmX9 z`7L-VVMa1XC4AHYhaBF|=+!{Djvl@~A=kiUEKx%%UqoJbKYhokCQdXmvWF{UpcFHk zOh5tJ+A8*CK_!de(Zv*eU8nw>dRd@7(uVeHLNURg@^;Mt-Mu4`8qXn{8Lq;sTYnm+ zrMN^eu}TuW7Q%=A`Yf`cM*Ff zvwqpMJri!pP4&H*XbW^umeR&hAmCesf1B(Vm0TpX;#EN$fw8xA(4f{ajuf4o+AYRK{981$=nB*>w(~w_st=8u z2pid+g+2a_L~z6Yq}6ZdX{IspNQ;fQ4O{xLcMsMn)cv#S<&}G6LgmUr4|KdrpD;ui zI-8pjK0)D~`8JFaj|(N>3)TW7E73wTl_YYr8(tPr!cjA*LG$c|qk4rDU1yIOB(syk z3GTDKw=2+|muvauUUah}1Z_%1a79O+k7N<)%Tz3)s7!Qkd;j}+odZR*>`$E)R+zg4 zr&PDd3coHCZ_eDPn35{SXl*YTV5ZnudpMHAy~#|osGPoL@;1yaI-7N^9miMqa#AHb z)%W`Hm>jMLm6kz&a55?$OrHQ?+E7SRS419XmkUmMB^`SLq~w9FAdqLEU}=X=nNA&j z{!G1-6>YdqYAL`r#$6Kai*wysg4T&2FqbS&;u|l+8Tpky)y3!(xELP#ZF2nd%*gW; z`fTL}BK#kea?GM=Xvp@7(-R*4io&V3&VTr83vd5tI>cXNp}+lp z;=={$$nK{*#)*qIP({C5ATBCFOTL*IE@}xL_{x^fPcLvQ>!4d;i?NaqW91=!X)roo zkBy~`3ZV@orFTD3ws<;G3soFRf87Kvfdyz89w>|8ffx5AhypO#JviIf5ETxkta2?x zHGt0E>Pd)d0sHd=LwGT$y$=L`zuJ*s#|tZmZf9jn{|MF*5QaEN3aLlvkk~~-soea* zkiVY*BZx61^PJsz)`pKd`55zb+EO7HkN0k$F|hrcfcdOiDCaW4jmMJAn=l@!0{6Ph zhLr`LFrIY!4cOI|R1z+0o1?pLIK4NSxwl!uW-*!sxcsa|8n;{?r9Zjpxp>L8MgR04 zZtFBX3>8+Z^_H5#{oWvt)wcG;Q{MdTb32y`JdAn56hSU^&3Y|#4UN7RB-;qlL(6O8 zt#XGjDdK-qS=&b^#=1ZnAFkVk`DC|gv%jIN0hGrar$bo{=yhj34`o$^-g?m75u%PV zQlAZnEtM5NmtCOd0aO^IhI>4i7_-kmNP0(`}8^`s2GRZsNl#%05KyY}pD}w}jR#E7bXxCqf zq$>_q$BFGZ<$+cKtN1rW<44T&_g`I{%5+1M;%rh_6Vp>{CskLVW=dS^#3)xCqcLdY z(g~SLmle*v#RW+$F*e@ODxccR7e$vyOA?B5xm@tjg&KF_vE?)?mu>&zr`_;ttdO7W z^+ODI+!V*4;o@cZW6^<->!Cy&@yeMVNH^Y>XCq^2{HX8Jv|#J3A0RfN4vz_V8**mK zSM!fVCH}JAanO zJ&ebH4}>bo8%y+sZwF3%mDbCJ@S83Nm#Iapu6f(yM62w)&}Ib7-P^*Dc-uUR)>))* zXkXe#OH^J<3%1iuMw&6Anm?V(*_-Md069WD(-NSdFBT3_oxvg}secDTn$5z5Zm zmfH^#y?*Hn)f&RWe@{Ib#gwz#%6fF-1aD9vNTee}5zHc6x9R#|!pow!7=m8l!~D>W3weY2zAr z!SN6ibHRM%_hr#E>>{h?d{8uOepeeHYWI>VzR(mmUTz!bfkW2 z)>#IwwoEp9x}71Obi11RtM*;+|L%{UNK488dFo%@+2HZ}Fi`0T?|o70=Wv^c^YrJ% znZ=t_e?W23>+1QQGD&e{ZuZ7MCA&)1sG0Cn_nr{bT;{-!&muLP0(g1O-b1Kn55(Y8 z)s@|h4#pEzx=KA7zCVQAnHybX>vp8Ay_43Z6dN^Jb9mrGwsA%^haY=6Ln<*A34b$-O8{i=Z<*C?Waq z3=(iZwa}Z;>tv3Bbf$@mHYk-A!bE(*nO~v8NGj*>c=p}N`bW&q3eG3DfAC&e5wV^ZUey@ekE(6k%w!H*EZb*CoW$9F5L zMSFgn5Z5Hu{S(z0leMAu6{0C;eB_n;P!iBKC5>Owt7b4EnBl-;mR!F2tL($XGM`kl zylVE9Ym`&o_E2ejoa{F^$FD|UcJ5xoh{;N@6Sd&6xHG-0XX18C5eVby-uOI0oU57y znUU#U5{|zPj0v5H7<}w)d)z(e8Kz5{2Rn6V>Ht72Mj>(jOA9m3=d&u@f!&!@x%>z3`+{uXDa04Y z{VUq?^Yn!dVUoU~%#Rcw#Sy7bXAv6*a=S#Lw1`7uurV4thj#Qb7B6WudJYbc4#!Kt zNqVx#wgq%sko?G-CP^kYtTa;>lcTF|umhKco^qUNmT4LH!7<#kuGn)ihgY%ShvMcp zb69L+bm^+tq)q=Mu%gktD9MxS$#0~W8pbuQX4Eyozet%TSb-&Zt&*Jw<^WI8?v;kZ zkP$BzOYXp$sDusM|7-o9hpDJmMR~@K1Fccj18*qIju7J5c^%&S(}sZK?HrMr?#h|R z4hfNh7wkDHvSGsbzzpvi+hxiQ2W|&tn#WpqB69B@9t|emmo^Z+;7fqKBkp$$QnHbO zG8It^*$D+91q87Da@!BMw!bnuR(F)BgPV|xJEZT1atQloA|0AT-I0#lK?C!Deo*Mo zs2Cm-DkNz*e#ei@FcWWZeIUKpfo3D@)R_#A))qC}LRp8%+m z_ZL+U_6GZ3J{Q&7jN_z*EDSmVh{dp44KDs2nTA&xou5@PJn}{Z+6<4ZEn)BqO za|In=L{8&${tj+4bAT|eY7$KHpTKqQybS`o23AQZ3YUDP$@v-c+~SKzPx*G><1VT= zbCxQ-=$=^+%#clKp%T8}a()%M{2+>Y0~}NDM9D~M-f3uzd)39cWzJgsrw|picN5HJ z=!vGgabt@X%%nbYRi#!i;2)xJgm`};5k_MonPpO^#ICg#;5grJ#Xz@{&XO|_Pp5Pt zV;68`5w<~F1ZJ$COvXx>lIM5*Tr>-OGnqy7MXJsn+n4Vq7T;oaXxV2*P+AI z=rPU!&eYJ=Lms1BulGr1)vHvze)k)wb~5&V(wwzqmolf?h1?Srj6h={#3QQV9v`8g zy|#LX`em^zf6pVPa51!*<+B~vCA6b)N9%P#Z1>4xs@vLotAt1y_x?X-fazXMg|SzU zt`%K%v2D$&h475|YSaBuwPWmTHeJmT#IjbZnm2}-aYQ`aJXvL3vu>TtyAJd#d1L3g zEuGN2k<7=NCNzc!>4@&~-iD1Sl-8~sT6S9AOcJ)K%Wm>veGKSQPHV#X^g{+(6xe+XNu z@XVq0Lr+x$uiVFO_=huv{DRz`&$^tdn34_Se5fu{K1_LfJLmMIRgMiY; zf)hZzod6em|9Y>~&RtT-cOb^zQc@(ZVb=nvx`=Ol$0j~(z{{z{d*^-ge(u1Gptt^b zi4{xzH{#+D+t&MF=R22+wXWYGIgQ?$Wh6NBLf?Ya2+EV@r;N;q1kb4pN^4gR`pQ&+ zHbsen9C`-am*qP~mOKH39- ztOw$&K|GnDZVSkYcUYopBrk;j(O~bUu#y1i4_^Dmv(-pt7)ir3U!nU%TrTf~M0==QvQIMS zDz>*{+_SOW$iPH2>R*9d$*`?V6ge;j_edbi{6J1y zb4>UR7O#EfzA3Ug1--ZvU2uTz7HaYJn?sb|t8vQ>n8Kk_s1pDhad~)I5YUAP0H}5u zsRiLUSPMq8qZYzPS1la5qFSg}a)9N%4S>-^#R^rcRAz!2jr`e_D%Yf1p?Yab)oV~m z3M{4)ASn4qiF0;sRjYz!*HSeI-hi@}1`jx#cNt5>@p*eSNEIiU6F(mRHgOuzSN^qH zJ)qL>Qc)5py}-Nj4;EB_kPQ_1l8aeD$+ND7S_I^3TGguKFu62fptO>fg60fL{GN!| Lu + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/public/logo.svg b/apps/web/public/logo.svg new file mode 100644 index 0000000..1f9a427 --- /dev/null +++ b/apps/web/public/logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/public/solarch.svg b/apps/web/public/solarch.svg new file mode 100644 index 0000000..9b6dfb5 --- /dev/null +++ b/apps/web/public/solarch.svg @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/src/api/ai.ts b/apps/web/src/api/ai.ts new file mode 100644 index 0000000..4da2183 --- /dev/null +++ b/apps/web/src/api/ai.ts @@ -0,0 +1,357 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { api, unwrap } from "./client"; +import { API_URL } from "../lib/env"; +import { getGuestToken, openGuestSignupModal } from "../lib/guest"; +import type { TabGraphData, TabGraphEdge, TabGraphMember } from "./tabs"; + +/** Session-less + guest-ticketed client (signup CTAs go to a modal instead of /billing). */ +const isGuestClient = (): boolean => + !(window as unknown as { Clerk?: { user?: unknown } }).Clerk?.user && !!getGuestToken(); + +export interface ChatResult { + reply: string; + applied?: { idMap: Record; nodeCount: number; edgeCount: number }; + attempts: number; +} + +/** Legacy one-shot — chat() endpoint (monolithic apply_architecture_graph). + * Prefer useAiChatStream for streaming. */ +export function useAiChat(projectId: string, tabId: string | null) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (message: string) => + unwrap( + await api.POST("/api/v1/projects/{projectId}/ai/chat", { + params: { path: { projectId } }, + body: { message, tabId: tabId ?? undefined } as never, + }), + ), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["tab-graph", projectId, tabId] }); + qc.invalidateQueries({ queryKey: ["tabs", projectId] }); + }, + }); +} + +// ──────────────────────────────────────────────────────────── +// Streaming agent — SSE + atomic create_node/create_edge tools +// ──────────────────────────────────────────────────────────── + +type StreamStatus = "idle" | "streaming" | "done" | "error" | "paused"; +export type AiMode = "agent" | "instruct"; + +interface BackendNode { + id: string; + type: string; + projectId: string; + position: { x: number; y: number }; + createdAt: string; + updatedAt: string; + version: number; + properties: Record; +} + +interface BackendEdge { + id: string; + projectId: string; + sourceNodeId: string; + targetNodeId: string; + kind: string; + createdAt: string; + updatedAt: string; + properties: Record; +} + +export interface AiStreamState { + status: StreamStatus; + mode: AiMode; + progress: { nodes: number; edges: number }; + /** instruct mode: live token-by-token accumulated text (typewriter). + * agent mode: unused (stays empty). */ + accumulatedText: string; + /** done event message (final for both modes). */ + message: string | null; + error: string | null; + /** Whether the error is retryable (provider/timeout/connection) → show "Try again". + * Plan/quota (402) is NOT retryable (upgrade required). */ + retryable: boolean; +} + +// ── Active stream counter ───────────────────────────────────────────── +// Canvas auto-triggers arrange on NEW edges arriving during AI generation; +// manually drawn edges don't trigger it. A module-level counter provides this +// distinction (independent of hook instances — OmniBar + InlineAiPrompt may run at once). +let activeStreams = 0; +let lastStreamEndAt = 0; +/** Whether AI generation is active — stream open OR just closed (post-stream + * invalidate/refetch edges also count as AI generation, trigger arrange). */ +export function isAiActive(graceMs = 4000): boolean { + return activeStreams > 0 || Date.now() - lastStreamEndAt < graceMs; +} + +/** Live-listen to generated element ids (the inline suggestion flow fills its + * pending set with these). Called AFTER the cache update. */ +export interface AiStreamCallbacks { + onNode?: (id: string) => void; + onEdge?: (id: string) => void; + /** Backend terminal rollback — element deleted from DB, must drop from the set too. */ + onRemoved?: (id: string, kind: "node" | "edge") => void; +} + +/** AI architect streaming — backend pushes SSE event after each create_node/create_edge + * tool execution. Hook incrementally updates React Query cache; canvas buildScene is + * diff-aware so new nodes appear with pop animation. + * + * start(message): open new stream (abort previous if any). + * abort(): close active stream. */ +export function useAiChatStream(projectId: string, tabId: string | null, callbacks?: AiStreamCallbacks) { + const qc = useQueryClient(); + const esRef = useRef(null); + // Callback identity can change on every render (inline object) — read via ref + // so that start() stays stable. + const cbRef = useRef(callbacks); + useEffect(() => { + cbRef.current = callbacks; + }, [callbacks]); + // Last request (message+mode) — "Continue" reopens the same request with continue=true. + const lastReqRef = useRef<{ message: string; mode: AiMode }>({ message: "", mode: "agent" }); + // React buffering: text-delta chunks land in useRef buffer; flushed to setState + // once per frame via rAF → 60fps maintained, no render spam. + const textBufferRef = useRef([]); + const flushScheduledRef = useRef(false); + + const [state, setState] = useState({ + status: "idle", + mode: "agent", + progress: { nodes: 0, edges: 0 }, + accumulatedText: "", + message: null, + error: null, + retryable: false, + }); + + const flushText = useCallback(() => { + flushScheduledRef.current = false; + const chunks = textBufferRef.current; + if (chunks.length === 0) return; + textBufferRef.current = []; + const concat = chunks.join(""); + setState((s) => ({ ...s, accumulatedText: s.accumulatedText + concat })); + }, []); + + const close = useCallback(() => { + if (esRef.current) { + esRef.current.close(); + esRef.current = null; + activeStreams = Math.max(0, activeStreams - 1); + if (activeStreams === 0) lastStreamEndAt = Date.now(); + } + }, []); + + const abort = useCallback(() => { + close(); + setState((s) => ({ ...s, status: s.status === "streaming" ? "idle" : s.status })); + }, [close]); + + const start = useCallback( + (message: string, mode: AiMode = "agent", continueRun = false) => { + if (!projectId || !message.trim()) return; + close(); // close previous stream if any + textBufferRef.current = []; + lastReqRef.current = { message, mode }; // store for "Continue" + + // Empty = relative (same-origin). EventSource in same-origin automatically + // sends Clerk cookie (cannot send Authorization header). + const baseUrl = API_URL; + const params = new URLSearchParams({ message, mode }); + if (tabId) params.set("tabId", tabId); + // "Continue": resume generation paused at step limit — backend sees existing + // graph and completes gaps (won't re-create existing ones). + if (continueRun) params.set("continue", "true"); + // Idempotency key — once per submission. EventSource auto-reconnect reopens + // the same URL (same requestId) → backend rejects duplicate generation + // (double billing + double nodes). + params.set("requestId", crypto.randomUUID()); + const url = `${baseUrl}/api/v1/projects/${projectId}/ai/chat/stream?${params.toString()}`; + + setState({ + status: "streaming", + mode, + progress: { nodes: 0, edges: 0 }, + accumulatedText: "", + message: null, + error: null, + retryable: false, + }); + + const es = new EventSource(url, { withCredentials: true }); + esRef.current = es; + activeStreams += 1; // close() decrements (done/paused/error/abort/unmount all go through close) + + es.addEventListener("node", (e) => { + const node = JSON.parse((e as MessageEvent).data) as BackendNode; + const member: TabGraphMember = { + id: node.id, + type: node.type, + properties: node.properties, + position: node.position, + version: node.version, + isReference: false, + }; + // UPSERT — agent can now update an existing node and re-emit the same id + // (refactor). Instead of append, replace if id exists (no duplicate). + let isNew = true; + qc.setQueryData(["tab-graph", projectId, tabId], (old) => { + if (!old) return old; + isNew = !old.nodes.some((n) => n.id === member.id); + return { + ...old, + nodes: isNew ? [...old.nodes, member] : old.nodes.map((n) => (n.id === member.id ? member : n)), + }; + }); + // Counter increments only on creation; an update mustn't inflate "N nodes created". + if (isNew) setState((s) => ({ ...s, progress: { ...s.progress, nodes: s.progress.nodes + 1 } })); + cbRef.current?.onNode?.(node.id); + }); + + es.addEventListener("edge", (e) => { + const edge = JSON.parse((e as MessageEvent).data) as BackendEdge; + const item: TabGraphEdge = { + id: edge.id, + kind: edge.kind, + sourceNodeId: edge.sourceNodeId, + targetNodeId: edge.targetNodeId, + }; + qc.setQueryData(["tab-graph", projectId, tabId], (old) => + old ? { ...old, edges: [...old.edges, item] } : old, + ); + setState((s) => ({ ...s, progress: { ...s.progress, edges: s.progress.edges + 1 } })); + cbRef.current?.onEdge?.(edge.id); + }); + + es.addEventListener("removed", (e) => { + // Terminal rollback — backend deleted orphan node; remove from cache. + // (On error path done doesn't invalidate; this listener is the only cleanup.) + const { id, kind } = JSON.parse((e as MessageEvent).data) as { id: string; kind: "node" | "edge" }; + qc.setQueryData(["tab-graph", projectId, tabId], (old) => { + if (!old) return old; + if (kind === "node") { + return { + ...old, + nodes: old.nodes.filter((n) => n.id !== id), + edges: old.edges.filter((ed) => ed.sourceNodeId !== id && ed.targetNodeId !== id), + }; + } + return { ...old, edges: old.edges.filter((ed) => ed.id !== id) }; + }); + cbRef.current?.onRemoved?.(id, kind); + }); + + es.addEventListener("text-delta", (e) => { + const { delta } = JSON.parse((e as MessageEvent).data) as { delta: string }; + textBufferRef.current.push(delta); + if (!flushScheduledRef.current) { + flushScheduledRef.current = true; + requestAnimationFrame(flushText); + } + }); + + es.addEventListener("done", (e) => { + const payload = JSON.parse((e as MessageEvent).data) as { message: string; counts?: { nodes: number; edges: number } }; + // Final flush — drain remaining buffer if any + if (textBufferRef.current.length > 0) flushText(); + // Align progress with backend truth — after orphan rollback 'removed' + // events don't decrement the counter, prevent inflated summary. + setState((s) => ({ ...s, status: "done", message: payload.message, progress: payload.counts ?? s.progress })); + close(); + if (mode === "agent") { + // Truth sync — nodes/edges created in agent mode, align cache with backend + qc.invalidateQueries({ queryKey: ["tab-graph", projectId, tabId] }); + qc.invalidateQueries({ queryKey: ["tabs", projectId] }); + } + // 4h quota counter consumed → refresh the remaining-allowance badge. + qc.invalidateQueries({ queryKey: ["subscription"] }); + }); + + es.addEventListener("paused", (e) => { + // Step limit reached, work incomplete — orphans PRESERVED. Resumes with "Continue". + const payload = JSON.parse((e as MessageEvent).data) as { message: string; counts?: { nodes: number; edges: number } }; + if (textBufferRef.current.length > 0) flushText(); + setState((s) => ({ ...s, status: "paused", message: payload.message, progress: payload.counts ?? s.progress })); + close(); + // Partial generation written to DB → align cache with backend. + qc.invalidateQueries({ queryKey: ["tab-graph", projectId, tabId] }); + qc.invalidateQueries({ queryKey: ["tabs", projectId] }); + qc.invalidateQueries({ queryKey: ["subscription"] }); + }); + + es.addEventListener("error", (e) => { + const data = (e as MessageEvent).data; + let errMsg = "AI connection lost."; + let code: string | undefined; + if (typeof data === "string") { + try { + const parsed = JSON.parse(data); + errMsg = parsed.message ?? errMsg; + code = parsed.code; + } catch { /* ignore */ } + } + // Duplicate connection (reconnect dedupe) — original stream continues, don't show error to user. + if (code === "ERR_DUPLICATE_REQUEST") { + close(); + return; + } + // Plan / quota denial — SSE errors never reach the global mutationCache. + if (code && (code.startsWith("ERR_PLAN_") || code.startsWith("ERR_QUOTA"))) { + const notice = errMsg && errMsg !== "AI connection lost." ? errMsg : "You've reached your plan limit."; + toast.error(notice); // show immediately (on the AI page) + setState((s) => ({ ...s, status: "error", error: notice, retryable: false })); + close(); + // Refresh subscription cache → OmniBar returns to the quota-full bar (countdown + CTA). + qc.invalidateQueries({ queryKey: ["subscription"] }); + + // 4h window elapsed (ERR_PLAN_METER): stay on page — the bar already + // shows "resets in Xh" + signup/upgrade CTA; no hard redirect. + if (code === "ERR_PLAN_METER") return; + + // Plan block (not quota): guests to the signup modal, registered users to /billing. + if (isGuestClient()) { + openGuestSignupModal(); + return; + } + try { + sessionStorage.setItem("solarch:billing-notice", notice); // re-show on /billing after reload + } catch { + /* sessionStorage unavailable → toast + panel still show it */ + } + if (window.location.pathname !== "/billing") { + setTimeout(() => window.location.assign("/billing"), 1400); + } + return; + } + // Provider/timeout/connection error → retryable (retry via lastReqRef). + setState((s) => ({ ...s, status: "error", error: errMsg, retryable: true })); + close(); + }); + }, + [projectId, tabId, qc, close, flushText], + ); + + // "Continue" — reopen last request with continue=true (resumes where it left off). + const continueRun = useCallback(() => { + const { message, mode } = lastReqRef.current; + if (message) start(message, mode, true); + }, [start]); + + // "Try again" — after a provider/timeout error, run the last request from SCRATCH (not continue). + const retry = useCallback(() => { + const { message, mode } = lastReqRef.current; + if (message) start(message, mode, false); + }, [start]); + + useEffect(() => close, [close]); // unmount cleanup + + return { ...state, start, abort, continueRun, retry }; +} diff --git a/apps/web/src/api/api-keys.ts b/apps/web/src/api/api-keys.ts new file mode 100644 index 0000000..d85ce8c --- /dev/null +++ b/apps/web/src/api/api-keys.ts @@ -0,0 +1,58 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getClerkToken } from "./client"; + +/** API key metadata — the plaintext key is returned only once, in the create response. */ +export interface ApiKeyRecord { + id: string; + name: string; + prefix: string; + createdAt: string; + lastUsedAt: string | null; +} + +async function request(path: string, init?: RequestInit): Promise { + const token = await getClerkToken(); + const res = await fetch(`/api/v1${path}`, { + credentials: "include", + ...init, + headers: { + ...(init?.body ? { "Content-Type": "application/json" } : {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...init?.headers, + }, + }); + const body = await res.json(); + if (!res.ok) { + throw Object.assign(new Error(body?.error?.message ?? "Request failed"), { + code: body?.error?.code, + }); + } + return body.data as T; +} + +export function useApiKeys() { + return useQuery({ + queryKey: ["api-keys"], + queryFn: () => request<{ keys: ApiKeyRecord[] }>("/api-keys").then((d) => d.keys), + }); +} + +export function useCreateApiKey() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (name: string) => + request<{ key: string } & ApiKeyRecord>("/api-keys", { + method: "POST", + body: JSON.stringify({ name }), + }), + onSuccess: () => void qc.invalidateQueries({ queryKey: ["api-keys"] }), + }); +} + +export function useDeleteApiKey() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => request<{ deleted: boolean }>(`/api-keys/${id}`, { method: "DELETE" }), + onSuccess: () => void qc.invalidateQueries({ queryKey: ["api-keys"] }), + }); +} diff --git a/apps/web/src/api/billing.ts b/apps/web/src/api/billing.ts new file mode 100644 index 0000000..7fbbfbb --- /dev/null +++ b/apps/web/src/api/billing.ts @@ -0,0 +1,92 @@ +import { useQuery, useMutation } from "@tanstack/react-query"; +import { getClerkToken } from "./client"; +import { guestHeaders } from "../lib/guest"; + +export type Plan = "guest" | "free" | "draw" | "build" | "code"; + +export interface BillingState { + plan: Plan; + status: string; + entitlements: { + canUseAI: boolean; + canCodegen: boolean; + /** Generate Code / ZIP export — Build and above. */ + canGenerateCode: boolean; + projectCap: number; + }; + /** 4-hour window caps (0 = disabled meter). codegen = Constructor + * "Generate Code" free preview (tiers without canGenerateCode get 1 per 4h). */ + meters: { generations: number; edits: number; questions: number; codegen: number }; + /** Usage in the active window. */ + usage: { generations: number; edits: number; questions: number; codegen?: number }; + /** End of the active 4h window (ISO) — "resets in Xh Ym" countdown. */ + windowResetAt: string; + periodEnd: string | null; + /** Trial end date (ISO) — only set while trialing (from Polar). */ + trialEndsAt: string | null; + cancelAtPeriodEnd: boolean; +} + +async function getJSON(path: string): Promise { + const token = await getClerkToken(); + const res = await fetch(`/api/v1${path}`, { + credentials: "include", + // Guests can read subscription status too (guest plan: 1 project, no AI). + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + const body = await res.json(); + if (!res.ok) { + throw Object.assign(new Error(body?.error?.message ?? "Request failed"), { code: body?.error?.code }); + } + return body.data as T; +} + +export function useSubscription() { + return useQuery({ + queryKey: ["subscription"], + queryFn: () => getJSON("/billing/subscription"), + staleTime: 30_000, + }); +} + +export function useCheckout() { + return useMutation({ + mutationFn: async (plan: Exclude) => { + const token = await getClerkToken(); + const res = await fetch("/api/v1/billing/checkout", { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ plan, returnUrl: `${window.location.origin}/billing` }), + }); + const body = await res.json(); + if (!res.ok) { + throw Object.assign(new Error(body?.error?.message ?? "Could not start checkout"), { code: body?.error?.code }); + } + // Polar hosted-redirect: backend returns the Polar checkout session URL. + return body.data as { url: string }; + }, + }); +} + +export async function openPortal() { + const d = await getJSON<{ url: string | null }>("/billing/portal"); + if (d.url) window.location.assign(d.url); +} + +/** Checkout success return — verify with Polar and persist the subscription; returns current state. */ +export async function confirmCheckout(checkoutId: string): Promise { + const token = await getClerkToken(); + const res = await fetch("/api/v1/billing/checkout/confirm", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: JSON.stringify({ checkoutId }), + }); + const body = await res.json(); + if (!res.ok) throw Object.assign(new Error(body?.error?.message ?? "Could not confirm purchase"), { code: body?.error?.code }); + return body.data as BillingState; +} diff --git a/apps/web/src/api/client.ts b/apps/web/src/api/client.ts new file mode 100644 index 0000000..5060840 --- /dev/null +++ b/apps/web/src/api/client.ts @@ -0,0 +1,90 @@ +import createClient from "openapi-fetch"; +import type { paths } from "./schema"; +import { getGuestToken } from "../lib/guest"; +import { API_URL } from "../lib/env"; + +// Empty = relative (same-origin). In dev Vite proxy (/api → :4000), in prod reverse +// proxy handles it; this way the Clerk httpOnly cookie (__session) flows to the backend. +const BASE_URL = API_URL; + +/** Clerk session token (Bearer). Supplement/backup to cookie: after signup/signin + * the __session cookie may not be written yet → getToken() always provides a fresh + * token, avoiding a 401 race on initial requests. Returns null if not signed in. */ +export async function getClerkToken(): Promise { + const clerk = (window as unknown as { + Clerk?: { session?: { getToken?: () => Promise } }; + }).Clerk; + try { + return (await clerk?.session?.getToken?.()) ?? null; + } catch { + return null; + } +} + +/** Typed openapi-fetch client. Path/param/body types come from schema.d.ts. + * credentials:"include" → cookie; additionally a Bearer token is attached to every request. */ +export const api = createClient({ baseUrl: BASE_URL, credentials: "include" }); + +// Backend clerkMiddleware accepts cookie OR Authorization Bearer. By attaching +// Bearer to every request we ensure robust auth independent of cookie timing. +// No Clerk session → guest ticket (X-Guest-Token) if present (login'siz deneme). +api.use({ + async onRequest({ request }) { + const token = await getClerkToken(); + if (token) { + request.headers.set("Authorization", `Bearer ${token}`); + } else { + const guest = getGuestToken(); + if (guest) request.headers.set("X-Guest-Token", guest); + } + return request; + }, +}); + +/** Solarch envelope: { success, data } | { success:false, error }. */ +export interface ErrorEnvelope { + success: false; + error: { + code: string; + message: string; + details?: { field: string; issue: string }[]; + suggestion?: string; // Fix suggestion on Rules Engine rejection + ruleViolated?: string; + docLink?: string; + currentVersion?: number; // version conflict + }; +} + +export class ApiError extends Error { + code: string; + details?: { field: string; issue: string }[]; + suggestion?: string; + constructor(code: string, message: string, details?: { field: string; issue: string }[], suggestion?: string) { + super(message); + this.name = "ApiError"; + this.code = code; + this.details = details; + this.suggestion = suggestion; + } +} + +/** Unwraps the envelope from an openapi-fetch result; throws ApiError on failure. */ +export function unwrap(res: { data?: unknown; error?: unknown }): T { + if (res.error) { + const e = res.error as ErrorEnvelope; + if (e && e.error) throw new ApiError(e.error.code, e.error.message, e.error.details, e.error.suggestion); + throw new ApiError("ERR_UNKNOWN", "An unexpected error occurred."); + } + const body = res.data as { success: boolean; data: T }; + return body.data; +} + +/** Shared error gate for raw fetch (raw.ts): if res.ok is false, throw ApiError + * from the envelope → no silent swallowing, error reaches global toast/caller. */ +export async function throwIfNotOk(res: Response): Promise { + if (res.ok) return; + let env: ErrorEnvelope | undefined; + try { env = (await res.json()) as ErrorEnvelope; } catch { /* no body / not JSON */ } + const e = env?.error; + throw new ApiError(e?.code ?? "ERR_UNKNOWN", e?.message ?? `HTTP ${res.status}`, e?.details, e?.suggestion); +} diff --git a/apps/web/src/api/codegen.ts b/apps/web/src/api/codegen.ts new file mode 100644 index 0000000..7439e2c --- /dev/null +++ b/apps/web/src/api/codegen.ts @@ -0,0 +1,427 @@ +/** Constructor codegen — backend generates a NestJS project skeleton from the node graph. + * schema.d.ts has no codegen path → typed api.POST is replaced by RAW fetch (raw.ts/client.ts + * pattern: getClerkToken Bearer + credentials:include + throwIfNotOk). Errors fall through to + * global mutationCache.onError (402 ERR_PLAN_CODEGEN → /billing is already handled there) — + * no local onError to avoid double-toast. */ + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { getClerkToken, throwIfNotOk } from "./client"; +import type { SystemMap } from "../features/simple/types"; +import { guestHeaders, openGuestSignupModal, getGuestToken } from "../lib/guest"; +import { API_URL } from "../lib/env"; + +/** A single generated file. Matches the backend contract. */ +export interface GeneratedFile { + path: string; + content: string; + language: "typescript" | "sql" | "json" | "markdown" | "env"; + /** Number of Surgical AI markers (edit points) in this file. */ + surgicalMarkers: number; +} + +/** Full generation result. Matches the backend contract. */ +export interface GeneratedProject { + target: "nestjs"; + files: GeneratedFile[]; + /** Wire phase: which node mapped to which files (nodeId → path[]). + * The "Show Code" flow uses this to focus on the relevant node's first file. */ + nodeFiles: Record; + summary: { + fileCount: number; + nodeCount: number; + surgicalMarkerCount: number; + /** Node kinds that were not included in generation → count (e.g. { note: 2 }). */ + skippedKinds: Record; + }; +} + +/** Codegen mutation. Not called if projectId is missing (button only visible on project route). + * data → GeneratedProject (envelope.data). Error: throwIfNotOk → ApiError → global toast. + * On success the project's codegenVersion is stamped to CODEGEN_VERSION backend-side, so we + * invalidate ["codegen-status", projectId] → generated catches up to current → the TopBar + * "Codebase improved — Update" prompt disappears. */ +export function useGenerateCode(projectId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ( + input?: { target?: "nestjs" }, + ): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + // No Clerk session → guest ticket; a ticketless request gets 401 and + // resets the guest session (see providers.handleAuthRedirect). + ...(token ? { Authorization: `Bearer ${token}` } : guestHeaders()), + }, + body: JSON.stringify({ target: input?.target ?? "nestjs" }), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: GeneratedProject }; + return body.data; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["codegen-status", projectId] }); + // Free preview meter consumed → refresh subscription status (gate the next open). + qc.invalidateQueries({ queryKey: ["subscription"] }); + }, + }); +} + +/** Revert a filled surgical region back to its stub — deletes the saved (AI/human) body. + * After success the caller regenerates (useGenerateCode) so the region shows as a stub + * again. Idempotent on the backend. */ +export function useRevertFill(projectId: string) { + return useMutation({ + mutationFn: async (input: { nodeId: string; member: string }): Promise => { + const token = await getClerkToken(); + const res = await fetch( + `/api/v1/projects/${projectId}/codegen/fill/${encodeURIComponent(input.nodeId)}/${encodeURIComponent(input.member)}`, + { + method: "DELETE", + credentials: "include", + headers: { ...(token ? { Authorization: `Bearer ${token}` } : guestHeaders()) }, + }, + ); + await throwIfNotOk(res); + }, + }); +} + +/** Codegen freshness for a project. + * - current: the Constructor version this build of the backend produces. + * - generated: the Constructor version the project was last generated with + * (null → never generated). + * - updateAvailable: generated != null && generated < current — i.e. an older + * codebase exists and a better one can now be produced. (Never + * generated → false: there is nothing to "update", only first-time + * generation.) */ +export interface CodegenStatus { + current: number; + generated: number | null; + updateAvailable: boolean; + /** The project's current structural graph revision. */ + graphRevision: number; + /** Graph revision stamped at generation time; null if never generated. */ + generatedGraphRevision: number | null; + /** Has the diagram changed structurally since generation (generated code is behind). */ + diagramDrifted: boolean; + /** Number of structural changes since generation. */ + driftCount: number; +} + +/** Reads the codegen freshness status for a project (ProjectAccessGuard on backend). + * Powers the TopBar "Codebase improved — Update" prompt. Disabled when projectId + * is missing so it never fires on non-project routes. */ +export function useCodegenStatus(projectId: string | undefined) { + return useQuery({ + queryKey: ["codegen-status", projectId], + enabled: !!projectId, + queryFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen/status`, { + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: CodegenStatus }; + return body.data; + }, + }); +} + +/** Simple View — the non-dev projection of the technical graph (feature map + capabilities). + * Generated deterministically backend-side (sibling of the Mermaid export); free, no AI. */ +export function useSimpleView(projectId: string | undefined) { + return useQuery({ + queryKey: ["simple-view", projectId], + enabled: !!projectId, + queryFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen/simple-view`, { + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: SystemMap }; + return body.data; + }, + }); +} + +export interface SimpleSketch { mermaid: string; source: "ai" | "deterministic" } + +/** Mermaid for the hand-drawn Simple sketch (AI-refined + cached server-side). */ +export function useSimpleSketch(projectId: string | undefined) { + return useQuery({ + queryKey: ["simple-sketch", projectId], + enabled: !!projectId, + staleTime: 5 * 60_000, + queryFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen/simple-sketch`, { + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: SimpleSketch }; + return body.data; + }, + }); +} + +/** Structured, Mermaid-free Simple-View model (ELK-laid-out + rough-rendered client-side). */ +export type SketchNodeKind = "feature" | "action" | "data" | "decision" | "external" | "state"; +export interface SketchModelNode { id: string; kind: SketchNodeKind; name: string; group?: string; color?: string } +export interface SketchModelEdge { from: string; to: string; label?: string } +export interface SketchModelGroup { id: string; name: string; color?: string } +export interface SimpleSketchModel { nodes: SketchModelNode[]; edges: SketchModelEdge[]; groups: SketchModelGroup[] } +/** `source`: did the AI refine names/colors ('ai') or is this the plain deterministic structure? + * `aiConfigured`: is the AI configured at all (key present)? Lets the UI tell "AI off" apart from + * "AI configured but the refine fell back" (source='deterministic' while aiConfigured=true). */ +export interface SimpleSketchModelResp { model: SimpleSketchModel; source: "ai" | "deterministic"; aiConfigured: boolean } + +export function useSimpleSketchModel(projectId: string | undefined, stage?: "baseline") { + return useQuery({ + queryKey: ["simple-sketch-model", projectId, stage ?? "full"], + enabled: !!projectId, + // No client stale window: the server already caches in the DB (cheap to re-fetch), so always + // pull fresh on open — otherwise a stale browser copy keeps showing an OLD diagram after the + // server has regenerated. refetchOnMount: 'always' re-checks every time Simple View opens. + staleTime: 0, + refetchOnMount: "always", + queryFn: async (): Promise => { + const token = await getClerkToken(); + const url = `/api/v1/projects/${projectId}/codegen/simple-sketch-model${stage ? `?stage=${stage}` : ""}`; + const res = await fetch(url, { + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: SimpleSketchModelResp }; + return body.data; + }, + }); +} + +/** Regenerate the Simple-View model — POSTs to bypass the server cache and re-run the AI refine, + * then writes the fresh result straight into the "full" query so the diagram updates in place. + * This is the "Regenerate" button: re-run the AI even when the graph hasn't changed. */ +export function useRegenerateSketchModel(projectId: string | undefined) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen/simple-sketch-model/regenerate`, { + method: "POST", + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: SimpleSketchModelResp }; + return body.data; + }, + onSuccess: (data) => { + qc.setQueryData(["simple-sketch-model", projectId, "full"], data); + }, + }); +} + +// ──────────────────────────────────────────────────────────── +// Surgical AI fill — SSE stream (server fills @solarch:surgical bodies) +// ──────────────────────────────────────────────────────────── + +const isGuestClient = (): boolean => + !(window as unknown as { Clerk?: { user?: unknown } }).Clerk?.user && !!getGuestToken(); + +export interface FillRegion { + status: "filled" | "violation" | "error"; + /** The region's node UUID — for the live "writing" animation (locates the file+nodeId+member region). */ + nodeId?: string; + member: string; + file: string; + attempts: number; + /** The filled body (when status="filled") — streamed into the editor with a typewriter effect. */ + body?: string; + /** WHY the region failed (status=violation/error): tsc/contract violations. Shown as + * "why" in the rail — the backend already carries this in the SSE region event. */ + violations?: string[]; + /** Error message if it could not be filled (error status). */ + error?: string; +} + +/** Verify/repair phase — project-wide progress (tsc/jest loop, live). + * `modgraph` = NestJS module-graph gate (boot-time DI: cycles / missing import-export) + * repaired deterministically; closes wiring errors that pass tsc but crash at boot. */ +export interface FillPhaseEntry { + kind: "verify" | "repair" | "imports" | "tests" | "modgraph"; + round?: number; + ok?: boolean; + errorCount?: number; + file?: string; + member?: string; + files?: number; + skipped?: boolean; + /** modgraph: number of deterministic repairs applied. */ + repairs?: number; + /** modgraph: remaining unrepairable findings (ideally 0). */ + findings?: number; +} + +/** Run mode: verified (tsc on the server) or draft (when the deps cache is absent). */ +export interface FillMode { + verified: boolean; + withTests: boolean; + reason?: string; +} + +/** Agent ACTIVITY (observation) — a single tool action of the fill agent (opencode-style live stream). + * Arrives from the backend SSE `activity` event. The summary is SAFE (NO code body / secret value). */ +export interface FillActivity { + member: string; + file: string; + tool: "read" | "grep" | "glob" | "lookup_members" | "verify_fill"; + summary: string; + ok?: boolean; + attempt?: number; +} + +export interface FillState { + status: "idle" | "streaming" | "done" | "error"; + fileCount: number; + markerCount: number; + /** Whether verified / jest enabled — filled when the `mode` event arrives. */ + mode: FillMode | null; + regions: FillRegion[]; + /** tsc/repair/test phase stream (live "watch the output"). */ + phases: FillPhaseEntry[]; + /** Agent activity stream (read/grep/verify_fill…) — opencode-style live watch. Capped. */ + activity: FillActivity[]; + filled: number; + violations: number; + errors: number; + /** Last tsc/test gate result (from the report event). */ + typecheck: { ok: boolean } | null; + tests: { ok: boolean; skipped?: boolean } | null; + /** Final filled project (the `files` event) — null until done. */ + files: GeneratedFile[] | null; + error: string | null; + /** Whether the error is transient/retryable (provider/timeout/ERR_FILL_UNVERIFIED) — show "Try + * again". Plan/quota errors (402) are NOT retryable (upgrade required). */ + retryable: boolean; +} + +const IDLE_FILL: FillState = { + status: "idle", fileCount: 0, markerCount: 0, mode: null, regions: [], phases: [], activity: [], + filled: 0, violations: 0, errors: 0, typecheck: null, tests: null, files: null, error: null, retryable: false, +}; + +/** Activity stream cap — a hard region calls many tools; keep only the last N so memory/render stay light. */ +const ACTIVITY_CAP = 300; + +/** Opens an EventSource to the Surgical AI fill stream. Accumulates per-region + * progress; on the terminal `files` event exposes the fully-filled project so the + * panel can swap the skeleton for the implemented code. Plan/quota denials (402) + * arrive as an SSE `error` event (never the global mutation toast) → handled here. */ +export function useFillStream(projectId: string | undefined) { + const qc = useQueryClient(); + const esRef = useRef(null); + const [state, setState] = useState(IDLE_FILL); + + const close = useCallback(() => { + if (esRef.current) { + esRef.current.close(); + esRef.current = null; + } + }, []); + + const start = useCallback((opts?: { jest?: boolean }) => { + if (!projectId) return; + close(); + setState({ ...IDLE_FILL, status: "streaming" }); + // jest ("deep verify") is optional: tsc is always in the loop; jest is slow → toggle. + const qs = opts?.jest ? "?jest=true" : ""; + const url = `${API_URL}/api/v1/projects/${projectId}/codegen/fill/stream${qs}`; + const es = new EventSource(url, { withCredentials: true }); + esRef.current = es; + + es.addEventListener("start", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { fileCount: number; markerCount: number }; + setState((s) => ({ ...s, fileCount: d.fileCount, markerCount: d.markerCount })); + }); + es.addEventListener("mode", (e) => { + const d = JSON.parse((e as MessageEvent).data) as FillMode; + setState((s) => ({ ...s, mode: d })); + }); + es.addEventListener("phase", (e) => { + const d = JSON.parse((e as MessageEvent).data) as FillPhaseEntry; + setState((s) => ({ ...s, phases: [...s.phases, d] })); + }); + // The CLI's actual count of regions to fill (may differ from the marker count) — + // use this as the counter denominator; arrives AFTER `start` and finalizes markerCount. + es.addEventListener("begin", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { total: number }; + setState((s) => ({ ...s, markerCount: d.total })); + }); + es.addEventListener("region", (e) => { + const d = JSON.parse((e as MessageEvent).data) as FillRegion; + setState((s) => ({ ...s, regions: [...s.regions, d] })); + }); + es.addEventListener("activity", (e) => { + const d = JSON.parse((e as MessageEvent).data) as FillActivity; + setState((s) => ({ ...s, activity: [...s.activity, d].slice(-ACTIVITY_CAP) })); + }); + es.addEventListener("report", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { + filled: number; violations: number; errors: number; + typecheck?: { ok: boolean }; tests?: { ok: boolean; skipped?: boolean }; + }; + setState((s) => ({ + ...s, filled: d.filled, violations: d.violations, errors: d.errors, + typecheck: d.typecheck ?? null, tests: d.tests ?? null, + })); + }); + es.addEventListener("files", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { files: GeneratedFile[] }; + setState((s) => ({ ...s, files: d.files, status: "done" })); + close(); + qc.invalidateQueries({ queryKey: ["codegen-status", projectId] }); + qc.invalidateQueries({ queryKey: ["subscription"] }); // generations quota consumed + }); + es.addEventListener("error", (e) => { + const data = (e as MessageEvent).data; + let msg = "Surgical AI connection lost."; + let code: string | undefined; + if (typeof data === "string") { + try { + const p = JSON.parse(data); + msg = p.message ?? msg; + code = p.code; + } catch { /* native close after done → no data */ } + } + // Native error after done → don't clobber done. + // Plan/quota errors are NOT retryable (upgrade required); others (provider/timeout/ + // ERR_FILL_UNVERIFIED/connection) are retryable → show "Try again". + const retryable = !(code && (code.startsWith("ERR_PLAN_") || code.startsWith("ERR_QUOTA"))); + setState((s) => (s.status === "done" ? s : { ...s, status: "error", error: msg, retryable })); + close(); + if (code && (code.startsWith("ERR_PLAN_") || code.startsWith("ERR_QUOTA"))) { + toast.error(msg); + qc.invalidateQueries({ queryKey: ["subscription"] }); + if (code === "ERR_PLAN_METER") return; + if (isGuestClient()) { openGuestSignupModal(); return; } + try { sessionStorage.setItem("solarch:billing-notice", msg); } catch { /* unavailable */ } + if (window.location.pathname !== "/billing") setTimeout(() => window.location.assign("/billing"), 1400); + } + }); + }, [projectId, qc, close]); + + const reset = useCallback(() => { close(); setState(IDLE_FILL); }, [close]); + + useEffect(() => close, [close]); // unmount cleanup + return { ...state, start, reset }; +} diff --git a/apps/web/src/api/edges.ts b/apps/web/src/api/edges.ts new file mode 100644 index 0000000..9b43f2a --- /dev/null +++ b/apps/web/src/api/edges.ts @@ -0,0 +1,43 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; +import { rawDeleteEdge } from "./raw"; + +export interface StoredEdge { + id: string; + projectId: string; + sourceNodeId: string; + targetNodeId: string; + kind: string; + properties: Record; + // non-blocking rules warning (e.g. WARN_COND_001 empty table) — returned on success path + warning?: { code: string; message: string; suggestion?: string }; +} + +export function useCreateEdge(projectId: string, tabId: string | null) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (input: { sourceNodeId: string; targetNodeId: string; kind: string }) => { + const res = await api.POST("/api/v1/projects/{projectId}/edges", { + params: { path: { projectId } }, + body: { + projectId, + sourceNodeId: input.sourceNodeId, + targetNodeId: input.targetNodeId, + kind: input.kind, + // IsAsync required; pub/sub edges are asynchronous. + properties: { IsAsync: ["PUBLISHES", "SUBSCRIBES"].includes(input.kind) }, + } as never, + }); + return unwrap(res); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["tab-graph", projectId, tabId] }), + }); +} + +export function useDeleteEdge(projectId: string) { + const qc = useQueryClient(); + // rawDeleteEdge: Bearer + credentials + throwIfNotOk (ApiError.code preserved) + invalidate. + return useMutation({ + mutationFn: (edgeId: string) => rawDeleteEdge(projectId, edgeId, qc), + }); +} diff --git a/apps/web/src/api/node-types.ts b/apps/web/src/api/node-types.ts new file mode 100644 index 0000000..a1a7393 --- /dev/null +++ b/apps/web/src/api/node-types.ts @@ -0,0 +1,41 @@ +import { useQuery } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; + +export interface FieldHint { + badge?: string; + group?: string; + /** Value-set registry id (e.g. 'http-methods', 'parameter-types'). + * Frontend opens a Select widget — fetched from /value-sets/:id. */ + valueSet?: string; + /** Node reference within the project — frontend opens NodeRefCombobox. + * If edgeKind is present: source → target edge is auto-created after selection/create. */ + nodeRef?: { + type: string; + edgeKind?: string; + }; +} + +export interface NodeTypeDetail { + id: string; + family: string; + familyLabel: string; + description: string; + nameKey: string; + schema: unknown; // JSON Schema (OpenAPI export) + fieldHints: Record; // dotted path → group/badge metadata +} + +/** Full detail of a single node type — Zod → JSON Schema + fieldHints. For Inspector. */ +export function useNodeType(id: string | null) { + return useQuery({ + queryKey: ["node-type", id], + enabled: !!id, + staleTime: Infinity, // node type schema does not change at runtime + queryFn: async () => + unwrap( + await api.GET("/api/v1/node-types/{typeId}", { + params: { path: { typeId: id! } }, + }), + ), + }); +} diff --git a/apps/web/src/api/nodes.ts b/apps/web/src/api/nodes.ts new file mode 100644 index 0000000..19f14e1 --- /dev/null +++ b/apps/web/src/api/nodes.ts @@ -0,0 +1,133 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; +import { rawDeleteNode } from "./raw"; + +export interface NodeTypeSummary { + id: string; + family: string; + familyLabel: string; + description: string; + nameKey: string; +} + +/** 21 node types — static, long cache. */ +export function useNodeTypes() { + return useQuery({ + queryKey: ["node-types"], + staleTime: Infinity, + queryFn: async () => { + const body = unwrap<{ types: NodeTypeSummary[]; total: number }>(await api.GET("/api/v1/node-types")); + return body.types; + }, + }); +} + +export function useCreateNode(projectId: string, tabId: string | null) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (input: { type: string; position: { x: number; y: number }; properties: Record }) => { + const res = await api.POST("/api/v1/projects/{projectId}/nodes", { + params: { path: { projectId } }, + body: { projectId, homeTabId: tabId ?? undefined, ...input } as never, + }); + return unwrap(res); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["tab-graph", projectId, tabId] }), + }); +} + +export interface StoredNode { + id: string; + projectId: string; + homeTabId: string; + type: string; + position: { x: number; y: number }; + properties: Record; + createdAt: string; + updatedAt: string; + version: number; // optimistic concurrency +} + +/** Single-node fetch for Inspector — runs when sidebar selection changes. */ +export function useNode(projectId: string, nodeId: string | null) { + return useQuery({ + queryKey: ["node", projectId, nodeId], + enabled: !!projectId && !!nodeId, + queryFn: async () => + unwrap( + await api.GET("/api/v1/projects/{projectId}/nodes/{nodeId}", { + params: { path: { projectId, nodeId: nodeId! } }, + }), + ), + }); +} + +/** Project-wide node list by type — data source for NodeRefCombobox autocomplete. + * Tab-agnostic: lists Exception/DTO/Enum etc. from all tabs. */ +export function useProjectNodes(projectId: string, type: string | null) { + return useQuery({ + queryKey: ["project-nodes", projectId, type], + enabled: !!projectId && !!type, + staleTime: 30_000, + queryFn: async () => { + const res = await api.GET("/api/v1/projects/{projectId}/nodes", { + params: { path: { projectId }, query: { type: type! } as never }, + }); + const body = unwrap<{ nodes: StoredNode[]; total: number }>(res); + return body.nodes; + }, + }); +} + +/** Node deletion — backend DELETE /projects/:pid/nodes/:nodeId, 204 No Content + DETACH (edge cascade). + * Uses raw fetch (path may be missing from openapi schema), error message extracted from envelope. + * Cache invalidate: all tab-graphs (homeTabId may differ from active tabId) + clear node cache. */ +export function useDeleteNode(projectId: string) { + const qc = useQueryClient(); + // rawDeleteNode: Bearer + credentials + throwIfNotOk (ApiError.code preserved) + + // tab-graph invalidate + node cache remove. Old manual fetch was giving 401 on + // cookie-race and losing the error code. + return useMutation({ + mutationFn: (nodeId: string) => rawDeleteNode(projectId, nodeId, qc), + }); +} + +/** Inspector PATCH — properties partial update. Called debounced for auto-save. + * Cache invalidate broader: all tab-graphs (homeTabId may differ from active tabId) + node cache. + * tabId param is now optional/legacy — unnecessary with broader invalidate. */ +export function useUpdateNode(projectId: string, nodeId: string | null, _tabId?: string | null) { + const qc = useQueryClient(); + void _tabId; + return useMutation({ + mutationFn: async (vars: { properties: Record; expectedVersion?: number }) => { + if (!nodeId) return undefined; + const res = await api.PATCH("/api/v1/projects/{projectId}/nodes/{nodeId}", { + params: { path: { projectId, nodeId } }, + // expectedVersion → backend optimistic concurrency guard (lost-update protection). + body: { properties: vars.properties, expectedVersion: vars.expectedVersion } as never, + }); + return unwrap(res); + }, + onSuccess: (result) => { + // Write new version to cache IMMEDIATELY → prevent stale-version false-conflict + // on consecutive autosaves (window before invalidate refetch). + if (result && nodeId) { + qc.setQueryData(["node", projectId, nodeId], (old) => + old ? { ...old, version: result.version } : old, + ); + } + qc.invalidateQueries({ queryKey: ["node", projectId, nodeId] }); + // tab-graph cache PREFIX match — canvas refreshes regardless of which tab the node is on + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + }, + onError: (err) => { + // Version conflict → server state + fresh version is re-fetched; Inspector + // useEffect replaces draft with server data when serverProperties changes. + const code = (err as { code?: string } | null)?.code; + if (code === "ERR_VERSION_CONFLICT") { + qc.invalidateQueries({ queryKey: ["node", projectId, nodeId] }); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + } + }, + }); +} diff --git a/apps/web/src/api/openapi.ts b/apps/web/src/api/openapi.ts new file mode 100644 index 0000000..c53f959 --- /dev/null +++ b/apps/web/src/api/openapi.ts @@ -0,0 +1,71 @@ +/** OpenAPI docs — the architecture graph projected to an OpenAPI 3.1 document the client renders + * with Scalar. Mirrors the Simple-View model hooks (useSimpleSketchModel / useRegenerateSketchModel): + * the GET serves the deterministic baseline instantly or the persisted AI-enriched ("documentized") + * doc; the POST forces a fresh grounded enrichment. Free, no billing gate (no code is generated). */ + +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getClerkToken, throwIfNotOk } from "./client"; +import { guestHeaders } from "../lib/guest"; + +/** Minimal structural view of the OpenAPI document (server type: `OpenAPIObject` from + * `@nestjs/swagger`). We only read `paths` (empty-state check) + `info`; Scalar consumes the + * whole object opaquely, so the rest stays loose. */ +export interface OpenApiDoc { + openapi: string; + info: { title: string; version: string; description?: string }; + paths: Record>; + components?: { schemas?: Record; securitySchemes?: Record }; + tags?: { name: string; description?: string }[]; +} + +/** `source`: did the AI annotate prose/examples ('ai') or is this the plain deterministic doc? + * `aiConfigured`: is the AI configured at all (key present)? Lets the UI tell "AI off" apart from + * "AI configured but the enrichment fell back" (source='deterministic' while aiConfigured=true). */ +export interface OpenApiResp { doc: OpenApiDoc; source: "ai" | "deterministic"; aiConfigured: boolean } + +/** The OpenAPI document for a project (ProjectAccessGuard on backend). + * `stage="baseline"` skips the AI for the instant deterministic doc. No client stale window: the + * server caches in the DB, so always pull fresh on open (refetchOnMount: 'always') — a stale browser + * copy would keep showing an OLD doc after the server regenerated. */ +export function useOpenApi(projectId: string | undefined, stage?: "baseline") { + return useQuery({ + queryKey: ["openapi", projectId, stage ?? "full"], + enabled: !!projectId, + staleTime: 0, + refetchOnMount: "always", + queryFn: async (): Promise => { + const token = await getClerkToken(); + const url = `/api/v1/projects/${projectId}/codegen/openapi.json${stage ? `?stage=${stage}` : ""}`; + const res = await fetch(url, { + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: OpenApiResp }; + return body.data; + }, + }); +} + +/** AI Documentize — POSTs to bypass the server cache and re-run the grounded enrichment, then writes + * the fresh result straight into the "full" query so the rendered docs update in place. The structure + * stays graph-true; only descriptions/examples on existing operations/schemas change. */ +export function useDocumentize(projectId: string | undefined) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/codegen/openapi/documentize`, { + method: "POST", + credentials: "include", + headers: token ? { Authorization: `Bearer ${token}` } : guestHeaders(), + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: OpenApiResp }; + return body.data; + }, + onSuccess: (data) => { + qc.setQueryData(["openapi", projectId, "full"], data); + }, + }); +} diff --git a/apps/web/src/api/patterns.ts b/apps/web/src/api/patterns.ts new file mode 100644 index 0000000..555c335 --- /dev/null +++ b/apps/web/src/api/patterns.ts @@ -0,0 +1,102 @@ +/** Pattern gallery — canonical seed patterns as starting points. + * GET /patterns (read-only, seed) drives the Welcome zero-state. Creating from a + * pattern is: create project → fetch the pattern's graph → atomic graph/apply. + * Raw fetch + envelope (same pattern as codegen.ts / review.ts). */ + +import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; +import { getClerkToken, throwIfNotOk } from "./client"; +import { guestHeaders } from "../lib/guest"; + +export interface PatternSummary { + id: string; + name: string; + description: string; + tags: string[]; + source: string; + createdAt: string; + nodeCount: number; + edgeCount: number; +} + +export interface PatternGraph { + nodes: { tempId: string; type: string; properties: Record }[]; + edges: { sourceTempId: string; targetTempId: string; edgeType: string; label?: string }[]; +} + +export interface StoredPattern extends PatternSummary { + graph: PatternGraph; +} + +async function authHeaders(): Promise> { + const token = await getClerkToken(); + return { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : guestHeaders()), + }; +} + +/** Canonical seed patterns (read-only). */ +export function usePatterns() { + return useQuery({ + queryKey: ["patterns"], + queryFn: async (): Promise => { + const res = await fetch("/api/v1/patterns", { credentials: "include", headers: await authHeaders() }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: PatternSummary[] }; + return body.data; + }, + }); +} + +/** Full pattern graphs (for the gallery previews) — one query per id, cached + * forever (seed patterns are immutable). Fetched lazily when the sheet opens. */ +export function usePatternDetails(ids: string[]) { + return useQueries({ + queries: ids.map((id) => ({ + queryKey: ["pattern", id], + staleTime: Infinity, + queryFn: async (): Promise => { + const res = await fetch(`/api/v1/patterns/${id}`, { credentials: "include", headers: await authHeaders() }); + await throwIfNotOk(res); + return ((await res.json()) as { data: StoredPattern }).data; + }, + })), + }); +} + +/** Create a new project and seed it with a pattern's (rules-legal) sub-graph. */ +export function useCreateFromPattern() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ name, patternId }: { name: string; patternId: string }): Promise<{ id: string }> => { + const headers = await authHeaders(); + + // 1. Create the project. + const pres = await fetch("/api/v1/projects", { + method: "POST", + credentials: "include", + headers, + body: JSON.stringify({ name }), + }); + await throwIfNotOk(pres); + const project = ((await pres.json()) as { data: { id: string } }).data; + + // 2. Fetch the pattern's full graph (apply wire format). + const gres = await fetch(`/api/v1/patterns/${patternId}`, { credentials: "include", headers }); + await throwIfNotOk(gres); + const pattern = ((await gres.json()) as { data: StoredPattern }).data; + + // 3. Atomically apply the sub-graph to the new project. + const ares = await fetch(`/api/v1/projects/${project.id}/graph/apply`, { + method: "POST", + credentials: "include", + headers, + body: JSON.stringify({ mutations: { nodes: pattern.graph.nodes, edges: pattern.graph.edges } }), + }); + await throwIfNotOk(ares); + + return { id: project.id }; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["projects"] }), + }); +} diff --git a/apps/web/src/api/projects.ts b/apps/web/src/api/projects.ts new file mode 100644 index 0000000..33b7b41 --- /dev/null +++ b/apps/web/src/api/projects.ts @@ -0,0 +1,43 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; + +export interface ProjectSummary { + id: string; + name: string; + description: string; + status: "draft" | "active" | "archived"; + createdAt: string; + updatedAt: string; + counts: { nodes: number; edges: number }; +} + +export function useProjects() { + return useQuery({ + queryKey: ["projects"], + queryFn: async () => { + // list endpoint returns { projects, total } → extract the projects array. + const body = unwrap<{ projects: ProjectSummary[]; total: number }>(await api.GET("/api/v1/projects")); + return body.projects; + }, + }); +} + +export function useCreateProject() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (name: string) => + unwrap(await api.POST("/api/v1/projects", { body: { name } as never })), + onSuccess: () => qc.invalidateQueries({ queryKey: ["projects"] }), + }); +} + +export function useDeleteProject() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + const res = await api.DELETE("/api/v1/projects/{projectId}", { params: { path: { projectId: id } } }); + if (res.error) throw new Error("Could not delete"); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["projects"] }), + }); +} diff --git a/apps/web/src/api/raw.ts b/apps/web/src/api/raw.ts new file mode 100644 index 0000000..70636f3 --- /dev/null +++ b/apps/web/src/api/raw.ts @@ -0,0 +1,102 @@ +/** Raw API calls outside hooks for undo/redo + autosave-flush. + * Hook onSuccess listeners are not triggered → no history loop. + * Auth: cookie + Bearer (getClerkToken) — same as openapi-fetch client, guards against + * cookie timing race. Errors: throwIfNotOk → no silent swallowing (caller/global handles). */ + +import type { QueryClient } from "@tanstack/react-query"; +import { getClerkToken, throwIfNotOk } from "./client"; +import { guestHeaders } from "../lib/guest"; + +async function authHeaders(json = true): Promise> { + const token = await getClerkToken(); + return { + ...(json ? { "Content-Type": "application/json" } : {}), + // Clerk oturumu yoksa misafir bileti (varsa) devreye girer. + ...(token ? { Authorization: `Bearer ${token}` } : guestHeaders()), + }; +} + +export const rawUpdateNodeProps = async ( + projectId: string, + nodeId: string, + properties: Record, + qc: QueryClient, + expectedVersion?: number, +) => { + const res = await fetch(`/api/v1/projects/${projectId}/nodes/${nodeId}`, { + method: "PATCH", + headers: await authHeaders(), + credentials: "include", + body: JSON.stringify({ properties, expectedVersion }), + }); + await throwIfNotOk(res); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + qc.invalidateQueries({ queryKey: ["node", projectId, nodeId] }); +}; + +export const rawDeleteNode = async ( + projectId: string, + nodeId: string, + qc: QueryClient, +) => { + const res = await fetch(`/api/v1/projects/${projectId}/nodes/${nodeId}`, { + method: "DELETE", + headers: await authHeaders(false), + credentials: "include", + }); + await throwIfNotOk(res); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + qc.removeQueries({ queryKey: ["node", projectId, nodeId] }); +}; + +export const rawCreateNode = async ( + projectId: string, + input: { type: string; homeTabId?: string; position: { x: number; y: number }; properties: Record }, + qc: QueryClient, +): Promise<{ id: string }> => { + const res = await fetch(`/api/v1/projects/${projectId}/nodes`, { + method: "POST", + headers: await authHeaders(), + credentials: "include", + body: JSON.stringify({ projectId, ...input }), + }); + await throwIfNotOk(res); + const body = await res.json(); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + return body?.data ?? body; +}; + +export const rawCreateEdge = async ( + projectId: string, + input: { sourceNodeId: string; targetNodeId: string; kind: string }, + qc: QueryClient, +): Promise<{ id: string; warning?: { code: string; message: string; suggestion?: string } }> => { + const res = await fetch(`/api/v1/projects/${projectId}/edges`, { + method: "POST", + headers: await authHeaders(), + credentials: "include", + body: JSON.stringify({ + projectId, + ...input, + properties: { IsAsync: ["PUBLISHES", "SUBSCRIBES"].includes(input.kind) }, + }), + }); + await throwIfNotOk(res); + const body = await res.json(); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); + return body?.data ?? body; +}; + +export const rawDeleteEdge = async ( + projectId: string, + edgeId: string, + qc: QueryClient, +) => { + const res = await fetch(`/api/v1/projects/${projectId}/edges/${edgeId}`, { + method: "DELETE", + headers: await authHeaders(false), + credentials: "include", + }); + await throwIfNotOk(res); + qc.invalidateQueries({ queryKey: ["tab-graph"] }); +}; diff --git a/apps/web/src/api/review.ts b/apps/web/src/api/review.ts new file mode 100644 index 0000000..be49a20 --- /dev/null +++ b/apps/web/src/api/review.ts @@ -0,0 +1,45 @@ +/** "Verify my architecture" — whole-graph rule review. + * POST /projects/:id/review re-evaluates every edge through the Rules Engine and + * returns a ranked Problems list. Deterministic (no LLM); same fetch+envelope + * pattern as codegen.ts. */ + +import { useMutation } from "@tanstack/react-query"; +import { getClerkToken, throwIfNotOk } from "./client"; +import { guestHeaders } from "../lib/guest"; + +export interface ReviewFinding { + severity: "error" | "warning"; + code: string; + message: string; + suggestion?: string; + ruleViolated?: string; + docLink?: string; + edgeId: string; + edgeKind: string; + /** [sourceId, targetId] — for focusEdge/focusNode. */ + nodeIds: string[]; +} + +export interface ReviewResult { + findings: ReviewFinding[]; + summary: { total: number; errors: number; warnings: number; clean: boolean }; +} + +export function useReviewArchitecture(projectId: string) { + return useMutation({ + mutationFn: async (): Promise => { + const token = await getClerkToken(); + const res = await fetch(`/api/v1/projects/${projectId}/review`, { + method: "POST", + credentials: "include", + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : guestHeaders()), + }, + }); + await throwIfNotOk(res); + const body = (await res.json()) as { success: boolean; data: ReviewResult }; + return body.data; + }, + }); +} diff --git a/apps/web/src/api/rules.ts b/apps/web/src/api/rules.ts new file mode 100644 index 0000000..8e61a82 --- /dev/null +++ b/apps/web/src/api/rules.ts @@ -0,0 +1,77 @@ +import { useQuery } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; + +export interface WhitelistRule { + source: string | string[]; + edge: string; + target: string | string[]; + layer?: string; + note?: string; +} + +/** Full rule matrix (whitelist) — static, long cache. */ +export function useRules() { + return useQuery({ + queryKey: ["rules"], + staleTime: Infinity, + queryFn: async () => { + const body = unwrap<{ whitelist: WhitelistRule[] }>(await api.GET("/api/v1/rules")); + return body.whitelist; + }, + }); +} + +const asArr = (v: string | string[]) => (Array.isArray(v) ? v : [v]); + +/** Allowed edge types for source → target (from the Rules Engine whitelist). */ +export function legalEdgeKinds(whitelist: WhitelistRule[], src: string, tgt: string): { edge: string; note?: string }[] { + const seen = new Set(); + const out: { edge: string; note?: string }[] = []; + for (const r of whitelist) { + if (asArr(r.source).includes(src) && asArr(r.target).includes(tgt) && !seen.has(r.edge)) { + seen.add(r.edge); + out.push({ edge: r.edge, note: r.note }); + } + } + return out; +} + +/** All (source type, edge kind) pairs that can connect to the target type — for input port drag. */ +export function legalSources( + whitelist: WhitelistRule[], + tgt: string, +): { nodeType: string; edge: string; note?: string }[] { + const seen = new Set(); + const out: { nodeType: string; edge: string; note?: string }[] = []; + for (const r of whitelist) { + if (!asArr(r.target).includes(tgt)) continue; + for (const s of asArr(r.source)) { + const key = `${s}::${r.edge}`; + if (!seen.has(key)) { + seen.add(key); + out.push({ nodeType: s, edge: r.edge, note: r.note }); + } + } + } + return out; +} + +/** All (target type, edge kind) pairs that can originate from the source type — for QuickConnectMenu. */ +export function legalTargets( + whitelist: WhitelistRule[], + src: string, +): { nodeType: string; edge: string; note?: string }[] { + const seen = new Set(); + const out: { nodeType: string; edge: string; note?: string }[] = []; + for (const r of whitelist) { + if (!asArr(r.source).includes(src)) continue; + for (const t of asArr(r.target)) { + const key = `${t}::${r.edge}`; + if (!seen.has(key)) { + seen.add(key); + out.push({ nodeType: t, edge: r.edge, note: r.note }); + } + } + } + return out; +} diff --git a/apps/web/src/api/schema.d.ts b/apps/web/src/api/schema.d.ts new file mode 100644 index 0000000..25e7024 --- /dev/null +++ b/apps/web/src/api/schema.d.ts @@ -0,0 +1,2642 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/v1/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Verifies that the service is up and running. Returns `{ status: 'ok', uptime }`. Used for liveness/readiness probes. + */ + get: operations["HealthController_check"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List projects + * @description Returns all projects (newest first). Includes `counts` (node + edge count) for each project. + */ + get: operations["ProjectsController_list"]; + put?: never; + /** + * Create new project + * @description Creates a new architecture project (workspace). The project must exist before adding nodes/edges. If `id` is not provided, the server generates a UUID. If `status` is not provided, defaults to `draft`. + */ + post: operations["ProjectsController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single project (+ counts) + * @description Returns the specified project with node/edge `counts`. + */ + get: operations["ProjectsController_getById"]; + put?: never; + post?: never; + /** + * Delete project (cascade) + * @description Permanently deletes the project **and all its nodes + edges** (cascade). Cannot be undone. + */ + delete: operations["ProjectsController_delete"]; + options?: never; + head?: never; + /** + * Update project + * @description Only `name`, `description`, and `status` can be updated (partial). `id` and timestamps are immutable. + */ + patch: operations["ProjectsController_update"]; + trace?: never; + }; + "/api/v1/projects/{projectId}/graph": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Full project graph + * @description Returns **all nodes + edges of the project in a single request**: `{ project, nodes[], edges[], counts }`. Ideal for loading the frontend canvas. + */ + get: operations["ProjectsController_getGraph"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/tabs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List tabs + * @description Sorted by order. + */ + get: operations["TabsController_list"]; + put?: never; + /** + * Create tab + * @description New context/canvas tab. moduleNodeId is optional (drill-down source). + */ + post: operations["TabsController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/tabs/{tabId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Tab detail */ + get: operations["TabsController_getById"]; + put?: never; + post?: never; + /** + * Delete tab + * @description Default tab cannot be deleted. Owned nodes are moved to Main Architecture, references are removed. + */ + delete: operations["TabsController_delete"]; + options?: never; + head?: never; + /** Update tab (name/order) */ + patch: operations["TabsController_update"]; + trace?: never; + }; + "/api/v1/projects/{projectId}/tabs/{tabId}/graph": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Tab render content + * @description Owned + referenced nodes (position + origin) + edges between them. + */ + get: operations["TabsController_graph"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/tabs/{tabId}/references/{nodeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Import node to tab / update reference position */ + put: operations["TabsController_addReference"]; + post?: never; + /** Remove reference (node is not deleted) */ + delete: operations["TabsController_removeReference"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/tabs/{tabId}/layout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Bulk save positions + * @description After drag: owned → node position, referenced → reference position. + */ + patch: operations["TabsController_layout"]; + trace?: never; + }; + "/api/v1/projects/{projectId}/nodes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List nodes + * @description Returns nodes in the project. Can be filtered to a single kind with the `?type=Table` query parameter. + */ + get: operations["NodesController_list"]; + put?: never; + /** + * Create new node + * @description Adds a new building block to the project. The body's **kind-specific `properties`** are validated with Zod based on the `type` (kind) discriminator. If `id`/timestamp is not provided, the server generates them. Project must exist (strict integrity), `*Name` must be unique within the project. + */ + post: operations["NodesController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/nodes/{nodeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single node + * @description Returns the specified node with its full properties. + */ + get: operations["NodesController_getById"]; + put?: never; + post?: never; + /** + * Delete node + * @description Deletes the node and its connected edges (DETACH). Not idempotent — returns 404 if not found. + */ + delete: operations["NodesController_delete"]; + options?: never; + head?: never; + /** + * Update node (field-level replace) + * @description `position` and/or `properties` are replaced with the provided full object (no deep merge). `type` (kind) **cannot be changed** — attempting it returns `ERR_KIND_IMMUTABLE`. + */ + patch: operations["NodesController_update"]; + trace?: never; + }; + "/api/v1/node-types": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all node types + * @description Returns a summary of 21 node types: `id`, `family` (Data/Business/Access/...), `nameKey` (unique field within project) and a short description. Populate the frontend's 'Add New Node' menu from this endpoint. + */ + get: operations["NodeTypesController_listAll"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/node-types/{typeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single node type (+ JSON Schema) + * @description The specified node type's metadata + **full JSON Schema** (generated with zodV3ToOpenAPI). The frontend renders its dynamic form from this schema. + */ + get: operations["NodeTypesController_getById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/node-types/{typeId}/rule": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Architecture rules for this node type + * @description This node type's role in the Rules Engine: which connections it is **allowed as source/target** (`allowAsSource`/`allowAsTarget`) and which are **denied** (`denyAsSource`/`denyAsTarget`, with ERR codes). + */ + get: operations["NodeTypesController_getRules"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/rules": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Architecture rule catalog + * @description Returns all rules from the Rules Engine: + * + * - **whitelist** (~32 rules): allowed source→edge→target combinations, split into 6 layers + * - **blacklist** (7 rules): strict denials `ERR_001..ERR_007` (message + suggestion) + * - **conditional** (3 rules): cyclic dependency, type mismatch, empty schema + * - **defaults**: unspecified connections are `deny` (default deny) + * + * Consult this catalog to learn which connections can be established between two nodes. + */ + get: operations["RulesController_catalog"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/edges": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List edges + * @description Returns edges in the project. Can be filtered with `?kind=CALLS`, `?sourceNodeId=...`, `?targetNodeId=...` (combinable). + */ + get: operations["EdgesController_list"]; + put?: never; + /** + * Create new edge (Rules Engine protected) + * @description Creates a directed connection between two nodes. The **Rules Engine** is enforced: connections not on the whitelist or caught by the blacklist are rejected (409). Self-loops and duplicates are also blocked. Source/target nodes + project must exist. + */ + post: operations["EdgesController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/edges/{edgeId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single edge + * @description Returns the specified edge with source/target/kind/properties. + */ + get: operations["EdgesController_getById"]; + put?: never; + post?: never; + /** + * Delete edge + * @description Deletes the connection. Nodes are not affected. + */ + delete: operations["EdgesController_delete"]; + options?: never; + head?: never; + /** + * Update edge (properties only) + * @description Only `properties` (Label/IsAsync/Protocol/RetryCount) can be updated. `kind`/`sourceNodeId`/`targetNodeId` **cannot be changed** — `ERR_EDGE_IMMUTABLE`. To change the connection, delete and recreate. + */ + patch: operations["EdgesController_update"]; + trace?: never; + }; + "/api/v1/projects/{projectId}/edges/validate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Pre-validate connection (does not write to DB) + * @description Runs the Rules Engine check **before** creating an edge — nothing is written to the DB. Called in the UI when the user draws an arrow or before the AI establishes a connection. Returns `{ isValid, severity?, engineResult? }`; `engineResult.suggestion` offers a fix recommendation. + */ + post: operations["EdgesController_validate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/edge-types": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List all edge types + * @description Returns 16 connection types: `id`, `family` (Communication/Data/Infrastructure/Architecture), description, example `source`/`target`, and direction note. The UI edge type picker is populated from this endpoint. + */ + get: operations["EdgeTypesController_listAll"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/edge-types/{edgeKind}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single edge type + * @description Detail of the specified edge type: family, description, example source/target, direction note. + */ + get: operations["EdgeTypesController_getById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/edge-types/{edgeKind}/rule": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Architecture rules for this edge type + * @description Returns the **allowed** (`allow` — which source→target pairs) and **denied** (`deny` — with ERR codes) rules for this edge type. + */ + get: operations["EdgeTypesController_getRules"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/graph/apply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Bulk apply architecture graph (AI batch) + * @description Processes multiple nodes + edges in a **single atomic transaction**. Used by the AI agent or frontend for bulk-save. + * + * Each node is validated with Zod, each edge with the Rules Engine + intra-batch cyclic dependency check. **Any violation rolls back the entire batch** (ROLLED_BACK) and returns `violations[]` + `suggestion`s — the AI reads these and self-corrects. + * + * Edges reference nodes by `tempId`; on success returns `idMap { tempId → permanent UUID }`. Positions are assigned server-side via auto-grid. + */ + post: operations["GraphController_apply"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/ai/chat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Chat with AI architect (generate architecture) + * @description Forwards a natural language request to the 'Chief Software Architect' AI. The AI sees the current graph (current_graph), generates architecture via the `apply_architecture_graph` tool, and **self-corrects** on Rules Engine violations (ReAct self-correction, max 3 attempts). Generation: Bedrock/Claude, tool calling. + * + * Response: `{ reply, applied?: {idMap, nodeCount, edgeCount}, attempts }`. If `applied` is populated, the architecture has been added to the canvas. + */ + post: operations["AiController_chat"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/patterns": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List patterns + * @description Summary of all patterns (including node/edge counts). + */ + get: operations["PatternsController_list"]; + put?: never; + /** + * Create pattern + * @description Reusable architecture sub-graph + description. The description is embedded and written to the vector index. + */ + post: operations["PatternsController_create"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/patterns/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Single pattern + * @description Full pattern including graphJson (sub-graph). + */ + get: operations["PatternsController_getById"]; + put?: never; + post?: never; + /** Delete pattern */ + delete: operations["PatternsController_delete"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/patterns/search": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Semantic pattern search + * @description Embeds the query and returns top-K cosine similarity from the native vector index. Returns empty list if no embeddings exist. + */ + post: operations["PatternsController_search"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/projects/{projectId}/patterns/promote": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Promote pattern from project graph + * @description Adds an existing project's graph (or a selected nodeIds sub-graph) to the pattern library. + */ + post: operations["PatternsController_promote"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + HealthController_check: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { status: 'ok', uptime: }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { projects: [...], total }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + /** @default */ + description?: string; + /** + * @default draft + * @enum {string} + */ + status?: "draft" | "active" | "archived"; + }; + }; + }; + responses: { + /** @description Project created. `data` returns the project + `counts: {nodes:0, edges:0}`. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID` — name/description missing or invalid status. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_ID_CONFLICT` — the provided `id` is already in use. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Project + counts. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deleted (no body). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name?: string; + description?: string; + /** @enum {string} */ + status?: "draft" | "active" | "archived"; + }; + }; + }; + responses: { + /** @description Updated project + counts. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ProjectsController_getGraph: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Full graph: project + nodes + edges + counts. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_list: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + /** Format: uuid */ + moduleNodeId?: string; + }; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_getById: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ERR_TAB_NOT_FOUND */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_delete: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ERR_TAB_DEFAULT_DELETE */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_update: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name?: string; + order?: number; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_graph: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_addReference: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + nodeId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + x: number; + y: number; + }; + }; + }; + responses: { + /** @description ERR_TAB_SELF_REFERENCE */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_removeReference: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + nodeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + TabsController_layout: { + parameters: { + query?: never; + header?: never; + path: { + projectId: string; + tabId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + items: { + /** Format: uuid */ + nodeId: string; + x: number; + y: number; + }[]; + }; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodesController_list: { + parameters: { + query?: { + /** @description Node kind filter (e.g. `Table`, `Service`). Invalid values are ignored. */ + type?: unknown; + }; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { nodes: [...], total }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodesController_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID that the node belongs to */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Table"; + properties: { + TableName: string; + Description: string; + Columns: { + /** @description Column name */ + Name: string; + /** + * @description SQL data type + * @enum {string} + */ + DataType: "INT" | "BIGINT" | "VARCHAR" | "TEXT" | "BOOLEAN" | "DATETIME" | "DATE" | "UUID" | "FLOAT" | "DECIMAL" | "JSON" | "ENUM"; + /** @description VARCHAR(n) length */ + Length?: number; + /** @description DECIMAL(p,s) precision */ + Precision?: number; + /** @description DECIMAL(p,s) scale */ + Scale?: number; + /** @description Single-column PK */ + IsPrimaryKey: boolean; + /** @description NOT NULL */ + IsNotNull: boolean; + /** @description UNIQUE */ + IsUnique: boolean; + /** @description AUTO_INCREMENT / SERIAL */ + AutoIncrement: boolean; + /** @description Default value expression */ + DefaultValue?: string; + /** @description Column comment */ + Comment?: string; + /** @description If DataType=ENUM → Enum node Name */ + EnumRef?: string; + /** @description GENERATED column */ + IsGenerated?: boolean; + /** @description Generated column expression */ + GeneratedExpression?: string; + }[]; + /** @description Composite PK (use Column.IsPrimaryKey for single-column) */ + PrimaryKey?: { + Columns: string[]; + }; + /** @default [] */ + ForeignKeys?: { + Name?: string; + Columns: string[]; + /** @description Target Table Name */ + ReferencesTable: string; + ReferencesColumns: string[]; + /** + * @default NO_ACTION + * @enum {string} + */ + OnDelete?: "CASCADE" | "RESTRICT" | "SET_NULL" | "NO_ACTION"; + /** + * @default NO_ACTION + * @enum {string} + */ + OnUpdate?: "CASCADE" | "RESTRICT" | "SET_NULL" | "NO_ACTION"; + }[]; + /** @default [] */ + UniqueConstraints?: { + Name?: string; + Columns: string[]; + }[]; + /** @default [] */ + CheckConstraints?: { + Name?: string; + Expression: string; + }[]; + /** @default [] */ + Indexes?: { + IndexName: string; + Columns: string[]; + /** + * @default BTree + * @enum {string} + */ + Type?: "BTree" | "Hash" | "GIN" | "GiST"; + /** @default false */ + IsUnique?: boolean; + IsPartial?: boolean; + WhereClause?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "DTO"; + properties: { + Name: string; + Description: string; + Fields: { + Name: string; + DataType: string; + IsRequired: boolean; + IsArray: boolean; + /** @default [] */ + ValidationRules?: { + /** @enum {string} */ + Rule: "Min" | "Max" | "MinLength" | "MaxLength" | "Email" | "Url" | "Regex" | "Pattern" | "Positive" | "Negative"; + /** @description Min/Max/Length value or Regex pattern */ + Value?: string; + }[]; + DefaultValue?: string; + /** @description → DTO node Name (nested DTO) */ + NestedDTORef?: string; + /** @description → Enum node Name */ + EnumRef?: string; + Description?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Model"; + properties: { + ClassName: string; + Description: string; + /** @description → Table node TableName ref */ + TableRef?: string; + Properties: { + Name: string; + Type: string; + /** @default false */ + IsNullable?: boolean; + /** @default false */ + IsCollection?: boolean; + /** @enum {string} */ + RelationType?: "OneToOne" | "OneToMany" | "ManyToOne" | "ManyToMany"; + /** @description → Model node ClassName ref */ + RelatedModelRef?: string; + }[]; + /** @default [] */ + Methods?: { + MethodName: string; + /** + * @default public + * @enum {string} + */ + Visibility?: "public" | "private" | "protected"; + /** @default [] */ + Parameters?: { + Name: string; + Type: string; + /** @default false */ + Optional?: boolean; + Default?: string; + }[]; + ReturnType: string; + /** @default false */ + IsAsync?: boolean; + /** @default false */ + IsStatic?: boolean; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Enum"; + properties: { + Name: string; + Description: string; + /** + * @default string + * @enum {string} + */ + BackingType?: "string" | "int"; + Values: { + Key: string; + /** @description Backing value (Key is used if not provided) */ + Value?: string; + Description?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "View"; + properties: { + ViewName: string; + Description: string; + /** @description SQL/aggregate definition */ + Definition: string; + /** @description → Table Names */ + SourceTables: string[]; + Materialized: boolean; + /** @default [] */ + Columns?: { + Name: string; + DataType: string; + }[]; + /** + * @description Refresh strategy for materialized view + * @enum {string} + */ + RefreshStrategy?: "onDemand" | "scheduled" | "onChange"; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Service"; + properties: { + ServiceName: string; + Description: string; + IsTransactionScoped: boolean; + Methods: { + MethodName: string; + /** + * @default public + * @enum {string} + */ + Visibility?: "public" | "private" | "protected"; + /** @default [] */ + Parameters?: { + Name: string; + Type: string; + /** @default false */ + Optional?: boolean; + Default?: string; + /** @description If parameter type is a DTO → DTO node Name */ + DtoRef?: string; + }[]; + ReturnType: string; + /** @description If return type is a DTO → DTO node Name */ + ReturnDtoRef?: string; + /** @default false */ + IsAsync?: boolean; + /** + * @description Throwable → Exception node Names + * @default [] + */ + Throws?: string[]; + Description?: string; + }[]; + /** @default [] */ + Dependencies?: { + /** @enum {string} */ + Kind: "Repository" | "Service" | "Cache" | "ExternalService"; + /** @description Dependent node's Name (DI) */ + Ref: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Worker"; + properties: { + WorkerName: string; + Description: string; + /** @description Cron expression */ + Schedule: string; + TaskToExecute: string; + TimeoutSeconds: number; + RetryPolicy: { + MaxRetries: number; + /** @enum {string} */ + BackoffStrategy?: "fixed" | "exponential"; + DelaySeconds?: number; + }; + Concurrency?: number; + /** @default true */ + IsEnabled?: boolean; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "EventHandler"; + properties: { + HandlerName: string; + Description: string; + EventName: string; + IsAsync: boolean; + /** @description Listens to → MessageQueue node Name */ + QueueRef?: string; + RetryPolicy?: { + MaxRetries: number; + DelaySeconds?: number; + }; + DeadLetterQueue?: string; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Controller"; + properties: { + ControllerName: string; + Description: string; + BaseRoute: string; + /** @description API version, e.g. 'v1' */ + Version?: string; + Endpoints: { + /** @enum {string} */ + HttpMethod: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + Route: string; + /** @description → DTO node Name */ + RequestDTORef?: string; + /** @description → DTO node Name */ + ResponseDTORef?: string; + RequiresAuth: boolean; + /** @default [] */ + RequiredRoles?: string[]; + /** @default [] */ + PathParams?: { + Name: string; + Type: string; + }[]; + /** @default [] */ + QueryParams?: { + Name: string; + Type: string; + /** @default false */ + Required?: boolean; + }[]; + /** @default [] */ + StatusCodes?: { + Code: number; + Description?: string; + }[]; + /** + * @description → Middleware node Names + * @default [] + */ + MiddlewareRefs?: string[]; + RateLimit?: { + Requests: number; + WindowSeconds: number; + }; + Description?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "MessageQueue"; + properties: { + QueueName: string; + Description: string; + /** @enum {string} */ + Type: "Queue" | "Topic"; + /** @enum {string} */ + Provider: "RabbitMQ" | "Kafka" | "AWS_SQS" | "Generic"; + /** @description Message body → DTO node Name */ + MessageFormat: string; + /** @enum {string} */ + DeliveryGuarantee?: "at-least-once" | "exactly-once" | "at-most-once"; + MaxRetries?: number; + DeadLetterQueue?: string; + RetentionSeconds?: number; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Repository"; + properties: { + RepositoryName: string; + Description: string; + /** @description Manages → Model/Table node Name */ + EntityReference: string; + /** @description Inherited repository base class */ + BaseClass?: string; + /** @default false */ + IsCached?: boolean; + /** @default [] */ + CustomQueries?: { + QueryName: string; + /** + * @default custom + * @enum {string} + */ + QueryType?: "find" | "findOne" | "aggregate" | "raw" | "custom"; + /** @default [] */ + Parameters?: { + Name: string; + Type: string; + }[]; + ReturnType: string; + Description?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Cache"; + properties: { + CacheName: string; + Description: string; + KeyPattern: string; + TTL_Seconds: number; + /** @enum {string} */ + Engine: "Redis" | "Memcached" | "Memory"; + /** @enum {string} */ + EvictionPolicy?: "LRU" | "LFU" | "FIFO" | "TTL"; + MaxSizeMB?: number; + /** @enum {string} */ + Serialization?: "json" | "binary" | "string"; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "ExternalService"; + properties: { + ServiceName: string; + Description: string; + /** Format: uri */ + BaseURL: string; + /** @enum {string} */ + AuthType: "None" | "Basic" | "Bearer" | "API_Key"; + TimeoutSeconds: number; + /** @default [] */ + Endpoints?: { + Name: string; + /** @enum {string} */ + Method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; + Path: string; + }[]; + RetryPolicy?: { + MaxRetries: number; + DelaySeconds?: number; + }; + RateLimit?: { + Requests: number; + WindowSeconds: number; + }; + CircuitBreaker?: { + FailureThreshold: number; + ResetSeconds: number; + }; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "FrontendApp"; + properties: { + AppName: string; + Description: string; + /** @enum {string} */ + Framework: "React" | "Vue" | "Angular" | "Svelte" | "Vanilla"; + /** @enum {string} */ + DeploymentType: "SPA" | "SSR" | "SSG"; + /** @enum {string} */ + StateManagement?: "Redux" | "Zustand" | "Context" | "Pinia" | "Vuex" | "NgRx" | "None"; + /** @enum {string} */ + StylingApproach?: "CSS" | "SCSS" | "Tailwind" | "StyledComponents" | "CSSModules"; + /** @default [] */ + Routes?: { + Path: string; + /** @description → UIComponent node Name */ + ComponentRef?: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "UIComponent"; + properties: { + ComponentName: string; + Description: string; + /** @default [] */ + Props?: { + Name: string; + Type: string; + /** @default false */ + Required?: boolean; + }[]; + /** @default [] */ + State?: { + Name: string; + Type: string; + }[]; + /** @default [] */ + Events?: { + Name: string; + PayloadType?: string; + }[]; + /** + * @description → UIComponent node Names + * @default [] + */ + ChildComponentRefs?: string[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Middleware"; + properties: { + MiddlewareName: string; + Description: string; + /** @enum {string} */ + AppliesTo: "Global" | "SpecificRoutes"; + ExecutionOrder: number; + /** @enum {string} */ + MiddlewareType?: "Auth" | "Logging" | "RateLimit" | "Cors" | "Compression" | "ErrorHandler" | "Custom"; + /** + * @description Middleware configuration key-value pairs + * @default [] + */ + Config?: { + Key: string; + Value: string; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "EnvironmentVariable"; + properties: { + Key: string; + Description: string; + /** @enum {string} */ + DataType: "String" | "Number" | "Boolean"; + IsSecret: boolean; + Environment: ("Dev" | "Staging" | "Prod")[]; + DefaultValue?: string; + /** @default true */ + IsRequired?: boolean; + /** @description Regex validation pattern */ + ValidationPattern?: string; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Exception"; + properties: { + ExceptionName: string; + Description: string; + HttpStatusCode: number; + /** @enum {string} */ + LogSeverity: "Info" | "Warning" | "Error" | "Critical"; + /** @description Application error code, e.g. ERR_USER_NOT_FOUND */ + ErrorCode?: string; + /** @description Inherited → Exception node Name */ + ParentExceptionRef?: string; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Module"; + properties: { + ModuleName: string; + Description: string; + StrictBoundaries: boolean; + /** + * @description Exposed → Service node Names (public API) + * @default [] + */ + ExposedServices?: string[]; + /** + * @description Depends on → Module node Names + * @default [] + */ + Dependencies?: string[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "APIGateway"; + properties: { + GatewayName: string; + Description: string; + /** @enum {string} */ + Provider: "Kong" | "Nginx" | "AWS_API_Gateway" | "Azure_API_Management" | "Generic"; + /** @enum {string} */ + AuthMode?: "None" | "JWT" | "OAuth2" | "ApiKey"; + CorsEnabled?: boolean; + /** @default [] */ + Routes?: { + Path: string; + /** @description → Controller or Service node Name */ + TargetRef: string; + Methods: ("GET" | "POST" | "PUT" | "DELETE" | "PATCH")[]; + /** @default false */ + AuthRequired?: boolean; + RateLimit?: { + Requests: number; + WindowSeconds: number; + }; + }[]; + }; + } | { + /** Format: uuid */ + projectId: string; + position: { + x: number; + y: number; + }; + /** Format: uuid */ + homeTabId?: string; + /** @enum {string} */ + type: "Orchestrator"; + properties: { + OrchestratorName: string; + Description: string; + /** @enum {string} */ + Pattern: "Saga" | "CompensatingTransaction" | "StateMachine" | "ProcessManager"; + /** @default [] */ + Steps?: { + StepName: string; + /** @description Step executor → Service node Name */ + ServiceRef: string; + Action: string; + /** @description Saga compensation action */ + CompensationAction?: string; + /** + * @default abort + * @enum {string} + */ + OnFailure?: "retry" | "compensate" | "abort"; + }[]; + }; + }; + }; + }; + responses: { + /** @description Node created — returns the full node object. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID` (schema) or `ERR_PROJECT_MISMATCH` (URL ≠ body projectId). */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND` — create the project first. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_ID_CONFLICT` (id exists) or `ERR_NAME_DUPLICATE` (*Name collision). */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodesController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Node UUID */ + nodeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Full node object. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_NODE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodesController_delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Node UUID */ + nodeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deleted (no body). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_NODE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodesController_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Node UUID */ + nodeId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + position?: { + x: number; + y: number; + }; + properties?: { + [key: string]: unknown; + }; + type?: string; + }; + }; + }; + responses: { + /** @description Updated node (`updatedAt` is refreshed). */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID` or `ERR_KIND_IMMUTABLE`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_NODE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodeTypesController_listAll: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { types: [...], total: 21 }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodeTypesController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Node kind (e.g. `Table`, `Service`, `Controller`) */ + typeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Metadata + `schema` (JSON Schema). */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_NODE_TYPE_NOT_FOUND` — valid types are listed in the message. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + NodeTypesController_getRules: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Node kind */ + typeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description allow/deny rule lists. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_NODE_TYPE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + RulesController_catalog: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description whitelist + blacklist + conditional + layers + counts. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_list: { + parameters: { + query?: { + /** @description Target node UUID filter. */ + targetNodeId?: unknown; + /** @description Source node UUID filter. */ + sourceNodeId?: unknown; + /** @description Edge kind filter (e.g. `CALLS`). */ + kind?: unknown; + }; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { edges: [...], total }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_create: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: uuid */ + projectId: string; + /** Format: uuid */ + sourceNodeId: string; + /** Format: uuid */ + targetNodeId: string; + /** @enum {string} */ + kind: "CALLS" | "REQUESTS" | "PUBLISHES" | "SUBSCRIBES" | "USES" | "HAS" | "EXTENDS" | "IMPLEMENTS" | "RETURNS" | "QUERIES" | "WRITES" | "CACHES_IN" | "DEPENDS_ON" | "READS_CONFIG" | "THROWS" | "ROUTES_TO"; + properties: { + Label?: string; + IsAsync: boolean; + /** @enum {string} */ + Protocol?: "HTTP" | "gRPC" | "TCP" | "WebSocket" | "AMQP" | "MQTT"; + RetryCount?: number; + }; + }; + }; + }; + responses: { + /** @description Edge created — full edge object. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID`, `ERR_PROJECT_MISMATCH`, or `ERR_EDGE_SELF_LOOP`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`, `ERR_EDGE_SOURCE_NOT_FOUND`, or `ERR_EDGE_TARGET_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_DUPLICATE`, `ERR_001..ERR_007` (blacklist), `ERR_COND_001/002` (conditional), or `ERR_NOT_WHITELISTED` (default deny). `error.suggestion` offers a fix recommendation. */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Edge UUID */ + edgeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Full edge object. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Edge UUID */ + edgeId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deleted (no body). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_update: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + /** @description Edge UUID */ + edgeId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + properties?: { + Label?: string; + IsAsync: boolean; + /** @enum {string} */ + Protocol?: "HTTP" | "gRPC" | "TCP" | "WebSocket" | "AMQP" | "MQTT"; + RetryCount?: number; + }; + kind?: string; + sourceNodeId?: string; + targetNodeId?: string; + }; + }; + }; + responses: { + /** @description Updated edge. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_SCHEMA_INVALID`, `ERR_EDGE_IMMUTABLE`, or `ERR_PATCH_EMPTY`. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgesController_validate: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: uuid */ + sourceNodeId: string; + /** Format: uuid */ + targetNodeId: string; + /** @enum {string} */ + kind: "CALLS" | "REQUESTS" | "PUBLISHES" | "SUBSCRIBES" | "USES" | "HAS" | "EXTENDS" | "IMPLEMENTS" | "RETURNS" | "QUERIES" | "WRITES" | "CACHES_IN" | "DEPENDS_ON" | "READS_CONFIG" | "THROWS" | "ROUTES_TO"; + }; + }; + }; + responses: { + /** @description Always 200. `data.isValid` true/false — rule result in `data.engineResult`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgeTypesController_listAll: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description `data: { types: [...], total: 16 }`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgeTypesController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Edge kind (e.g. `CALLS`, `WRITES`, `PUBLISHES`) */ + edgeKind: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Edge type metadata. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_TYPE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + EdgeTypesController_getRules: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Edge kind */ + edgeKind: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description allow + deny rule lists. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_EDGE_TYPE_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GraphController_apply: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: uuid */ + tabId?: string; + mutations: { + nodes: { + tempId: string; + type: string; + properties: { + [key: string]: unknown; + }; + }[]; + edges: { + sourceTempId: string; + targetTempId: string; + /** @enum {string} */ + edgeType: "CALLS" | "REQUESTS" | "PUBLISHES" | "SUBSCRIBES" | "USES" | "HAS" | "EXTENDS" | "IMPLEMENTS" | "RETURNS" | "QUERIES" | "WRITES" | "CACHES_IN" | "DEPENDS_ON" | "READS_CONFIG" | "THROWS" | "ROUTES_TO"; + label?: string; + }[]; + }; + }; + }; + }; + responses: { + /** @description `data.success=true` → idMap + nodeCount + edgeCount. `data.success=false` → transactionStatus ROLLED_BACK + violations[]. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AiController_chat: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + message: string; + /** @default [] */ + history?: { + /** @enum {string} */ + role: "user" | "assistant"; + content: string; + }[]; + /** Format: uuid */ + tabId?: string; + }; + }; + }; + responses: { + /** @description AI response + (if any) applied architecture. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_PROJECT_NOT_FOUND`. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description `ERR_AI_NOT_CONFIGURED` — LLM API key is missing. */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + description: string; + /** @default [] */ + tags?: string[]; + graph: { + nodes: { + tempId: string; + type: string; + properties: { + [key: string]: unknown; + }; + }[]; + /** @default [] */ + edges?: { + sourceTempId: string; + targetTempId: string; + /** @enum {string} */ + edgeType: "CALLS" | "REQUESTS" | "PUBLISHES" | "SUBSCRIBES" | "USES" | "HAS" | "EXTENDS" | "IMPLEMENTS" | "RETURNS" | "QUERIES" | "WRITES" | "CACHES_IN" | "DEPENDS_ON" | "READS_CONFIG" | "THROWS" | "ROUTES_TO"; + label?: string; + }[]; + }; + }; + }; + }; + responses: { + /** @description Created pattern summary. */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description ERR_EMBEDDINGS_NOT_CONFIGURED — no embedding provider configured. */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_getById: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Pattern UUID */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ERR_PATTERN_NOT_FOUND */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_delete: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Pattern UUID */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ERR_PATTERN_NOT_FOUND */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_search: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + query: string; + k?: number; + minScore?: number; + }; + }; + }; + responses: { + /** @description [{ pattern, score }] sorted by similarity. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PatternsController_promote: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Project UUID */ + projectId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + description: string; + /** @default [] */ + tags?: string[]; + nodeIds?: string[]; + }; + }; + }; + responses: { + /** @description ERR_PROJECT_NOT_FOUND / ERR_PATTERN_NODE_NOT_FOUND */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; +} diff --git a/apps/web/src/api/tabs.ts b/apps/web/src/api/tabs.ts new file mode 100644 index 0000000..8b6c96f --- /dev/null +++ b/apps/web/src/api/tabs.ts @@ -0,0 +1,125 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; + +export interface Tab { + id: string; + projectId: string; + name: string; + isDefault: boolean; + order: number; + moduleNodeId?: string; + createdAt: string; + updatedAt: string; +} + +export interface TabGraphMember { + id: string; + type: string; + properties: Record; + position: { x: number; y: number }; + version: number; // optimistic concurrency — backend sends via memberFrom + isReference: boolean; + origin?: string; + // Implementation counters (reported by the Solarch CLI / VS Code extension). + implTotal?: number; + implFilled?: number; + implAi?: number; +} +export interface TabGraphEdge { + id: string; + kind: string; + sourceNodeId: string; + targetNodeId: string; +} +export interface TabGraphData { + tab: Tab; + nodes: TabGraphMember[]; + edges: TabGraphEdge[]; +} + +export function useTabGraph(projectId: string, tabId: string | null) { + return useQuery({ + queryKey: ["tab-graph", projectId, tabId], + queryFn: async () => + unwrap( + await api.GET("/api/v1/projects/{projectId}/tabs/{tabId}/graph", { + params: { path: { projectId, tabId: tabId! } }, + }), + ), + enabled: !!projectId && !!tabId, + }); +} + +/** Save position after drag (owned → node.position, referenced → REFERENCES). + * Cache invalidate broader: so canvas refetch triggers on programmatic mutations + * like undo/redo. During drag the scene already does optimistic update — invalidate is idempotent. */ +export function useSaveLayout(projectId: string, tabId: string | null) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (items: { nodeId: string; x: number; y: number }[]) => { + if (!tabId) return; + await api.PATCH("/api/v1/projects/{projectId}/tabs/{tabId}/layout", { + params: { path: { projectId, tabId } }, + body: { items } as never, + }); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ["tab-graph"] }), + }); +} + +export function useTabs(projectId: string) { + return useQuery({ + queryKey: ["tabs", projectId], + queryFn: async () => + unwrap( + await api.GET("/api/v1/projects/{projectId}/tabs", { params: { path: { projectId } } }), + ), + enabled: !!projectId, + }); +} + +/** Create a new tab. moduleNodeId is optional (drill-down in the future). */ +export function useCreateTab(projectId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (name: string) => + unwrap( + await api.POST("/api/v1/projects/{projectId}/tabs", { + params: { path: { projectId } }, + body: { name } as never, + }), + ), + onSuccess: () => qc.invalidateQueries({ queryKey: ["tabs", projectId] }), + }); +} + +/** Update tab name or order. */ +export function useUpdateTab(projectId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async ({ tabId, name }: { tabId: string; name?: string }) => + unwrap( + await api.PATCH("/api/v1/projects/{projectId}/tabs/{tabId}", { + params: { path: { projectId, tabId } }, + body: { name } as never, + }), + ), + onSuccess: () => qc.invalidateQueries({ queryKey: ["tabs", projectId] }), + }); +} + +/** Delete tab. The default tab cannot be deleted (backend returns ERR_TAB_DEFAULT_DELETE). + * Owned nodes are moved to Main Architecture, references are removed. */ +export function useDeleteTab(projectId: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (tabId: string) => + api.DELETE("/api/v1/projects/{projectId}/tabs/{tabId}", { + params: { path: { projectId, tabId } }, + }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["tabs", projectId] }); + qc.invalidateQueries({ queryKey: ["tab-graph", projectId] }); + }, + }); +} diff --git a/apps/web/src/api/value-sets.ts b/apps/web/src/api/value-sets.ts new file mode 100644 index 0000000..c8c2478 --- /dev/null +++ b/apps/web/src/api/value-sets.ts @@ -0,0 +1,60 @@ +/** Value-sets API — Solarch's shared enum / lookup catalog. + * Fetched from fieldHint.valueSet references in Inspector forms. */ + +import { useQuery } from "@tanstack/react-query"; +import { api, unwrap } from "./client"; + +// openapi-fetch type layer doesn't know about value-sets endpoints yet +// (will be resolved once schema.d.ts is regenerated). Using any cast for now. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const apiAny = api as any; + +export interface ValueOption { + value: string; + label?: string; + description?: string; + group?: string; +} + +export interface ValueSet { + id: string; + label: string; + description: string; + values: ValueOption[]; +} + +export interface ValueSetSummary { + id: string; + label: string; + description: string; + count: number; +} + +/** List of all value-sets (summary). Static — staleTime Infinity. */ +export function useValueSets() { + return useQuery({ + queryKey: ["value-sets"], + staleTime: Infinity, + queryFn: async () => { + const body = unwrap<{ sets: ValueSetSummary[]; total: number }>( + await apiAny.GET("/api/v1/value-sets"), + ); + return body.sets; + }, + }); +} + +/** Single value-set with all its values. */ +export function useValueSet(id: string | null) { + return useQuery({ + queryKey: ["value-set", id], + enabled: !!id, + staleTime: Infinity, + queryFn: async () => + unwrap( + await apiAny.GET("/api/v1/value-sets/{id}", { + params: { path: { id: id! } }, + }), + ), + }); +} diff --git a/apps/web/src/app/AppShell.css b/apps/web/src/app/AppShell.css new file mode 100644 index 0000000..6c0922e --- /dev/null +++ b/apps/web/src/app/AppShell.css @@ -0,0 +1,22 @@ +.app-shell { + display: flex; + flex-direction: column; + height: 100vh; /* legacy browser fallback */ + height: 100dvh; /* mobile/tablet: account for the browser address+tool bar → the bottom + BottomBar isn't pushed outside the visible area (100vh counts it and clips the bar) */ + width: 100vw; + overflow: hidden; + /* iPad/touch: safe area so the bottom home-indicator bar doesn't cover the BottomBar */ + padding-bottom: env(safe-area-inset-bottom, 0px); + background: var(--paper); +} +.app-main { + flex: 1; + min-height: 0; + overflow: hidden; + position: relative; + /* Own stacking context: the body surfaces (Code/API/Docs panels at MODAL z) stay contained BELOW + the TopBar chrome, so the ViewSwitch sub-mode dropdowns render above them (not covered). */ + isolation: isolate; + background: var(--paper); +} diff --git a/apps/web/src/app/AppShell.tsx b/apps/web/src/app/AppShell.tsx new file mode 100644 index 0000000..92556bf --- /dev/null +++ b/apps/web/src/app/AppShell.tsx @@ -0,0 +1,150 @@ +import { useEffect, useState } from "react"; +import { Outlet } from "react-router-dom"; +import { TopBar } from "../components/TopBar"; +import { BottomBar } from "../components/BottomBar"; +import { EditorModal } from "../components/EditorModal"; +import { CommandPalette } from "../components/CommandPalette"; +import { DocsModal, type DocsSection } from "../components/DocsModal"; +import { useSelection } from "../state/selection"; +import { useCanvasCommands } from "../canvas/canvas-commands"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { ConfirmProvider } from "../components/ui/confirm-dialog"; +import { Toaster } from "sonner"; +import { Z_LAYERS } from "../lib/z-layers"; +import { useClaimGuestProject } from "../features/auth/useClaimGuestProject"; +import "./AppShell.css"; + +/** Main app shell — TopBar + canvas main + BottomBar + global modals. + * Global keyboard shortcuts live here (⌘K, ⌘E, ⌘⇧C, F2, Delete, Esc). */ +export function AppShell() { + const selectedNodeId = useSelection((s) => s.selectedNodeId); + const selectNode = useSelection((s) => s.selectNode); + // After sign-up/login, migrate the guest sketch to the account (if a ticket exists, one-shot). + useClaimGuestProject(); + const [paletteOpen, setPaletteOpen] = useState(false); + const [docsOpen, setDocsOpen] = useState(false); + const [docsSection, setDocsSection] = useState("nodes"); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + const t = e.target as HTMLElement | null; + const inForm = t?.tagName === "INPUT" || t?.tagName === "TEXTAREA" || t?.isContentEditable; + const mod = e.metaKey || e.ctrlKey; + + // ⌘K → toggle command palette (everywhere, even inside forms) + if (mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === "k") { + e.preventDefault(); + setPaletteOpen((v) => !v); + return; + } + + // Esc hierarchical close: NameEditor → EditorModal → Selection (bypassed when inside form) + if (e.key === "Escape" && !inForm) { + const s = useSelection.getState(); + if (s.nameEditorOpen) { s.closeNameEditor(); return; } + if (s.editorNodeId) { s.closeEditor(); return; } + if (selectedNodeId) { selectNode(null); return; } + } + + if (inForm) return; + + // ⌘E → toggle EditorModal (when a node is selected). openEditor/closeEditor are atomic. + if (mod && !e.shiftKey && !e.altKey && e.key === "e" && selectedNodeId) { + e.preventDefault(); + const s = useSelection.getState(); + if (s.editorNodeId) s.closeEditor(); + else s.openEditor(selectedNodeId); + return; + } + + // ⌘⇧C → copy selected node (NodeActionBar registers canvas-commands.copy) + if (mod && e.shiftKey && (e.key === "C" || e.key === "c") && selectedNodeId) { + e.preventDefault(); + useCanvasCommands.getState().copy?.(); + return; + } + + // F2 → rename (inline NameEditor) + if (e.key === "F2" && selectedNodeId) { + e.preventDefault(); + useSelection.getState().openNameEditor(); + return; + } + + // Delete / Backspace → directly delete selected node + if ((e.key === "Delete" || e.key === "Backspace") && selectedNodeId) { + e.preventDefault(); + useCanvasCommands.getState().deleteSelected?.(); + return; + } + }; + + // Global event for ⌘K toggle from BottomBar or other UI elements + const onCmdkEvent = () => setPaletteOpen((v) => !v); + const onDocsEvent = (e: Event) => { + const detail = (e as CustomEvent<{ section?: DocsSection }>).detail; + setDocsSection(detail?.section ?? "nodes"); + setDocsOpen(true); + }; + + window.addEventListener("keydown", onKey); + window.addEventListener("solarch:cmdk-open", onCmdkEvent); + window.addEventListener("solarch:docs-open", onDocsEvent); + return () => { + window.removeEventListener("keydown", onKey); + window.removeEventListener("solarch:cmdk-open", onCmdkEvent); + window.removeEventListener("solarch:docs-open", onDocsEvent); + }; + }, [selectedNodeId, selectNode]); + + return ( + + +

+ + + + ); +} + +/** Toggle ⌘K palette from UI — used by BottomBar button. */ +export function openCommandPalette(): void { + window.dispatchEvent(new CustomEvent("solarch:cmdk-open")); +} + +/** Toggle docs modal from UI. */ +export function openDocs(section: DocsSection = "nodes"): void { + window.dispatchEvent(new CustomEvent("solarch:docs-open", { detail: { section } })); +} diff --git a/apps/web/src/app/GlobalErrorBoundary.tsx b/apps/web/src/app/GlobalErrorBoundary.tsx new file mode 100644 index 0000000..db9a09c --- /dev/null +++ b/apps/web/src/app/GlobalErrorBoundary.tsx @@ -0,0 +1,40 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; + +/** App-root render error boundary. The Router's errorElement ONLY catches loader/action + * errors; throws during component render crash the entire app to a white screen. This + * boundary (wraps RouterProvider) catches those and shows a simple recovery screen. + * Error catching in React 19 still requires a class component. Since this is OUTSIDE + * the Router context, Link/useNavigate are unavailable — recovery uses window.location. */ +export class GlobalErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> { + state: { error: Error | null } = { error: null }; + + static getDerivedStateFromError(error: Error) { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + // Single extension-point: in the future Sentry.captureException(error, { extra: info }). + console.error("[solarch] uncaught render error:", error, info.componentStack); + } + + reset = () => this.setState({ error: null }); + + render() { + if (!this.state.error) return this.props.children; + const message = this.state.error.message || "Unknown error"; + return ( +
+
// something went wrong
+
{message}
+
+ + +
+
+ ); + } +} diff --git a/apps/web/src/app/ThemeController.tsx b/apps/web/src/app/ThemeController.tsx new file mode 100644 index 0000000..eb887e3 --- /dev/null +++ b/apps/web/src/app/ThemeController.tsx @@ -0,0 +1,63 @@ +/** ThemeController — bridges the theme store to the outside world (invisible, returns null). + * + * Two bridges: + * 1. OS preference: re-resolve when `prefers-color-scheme` changes (only mode==="system"). + * 2. Per-user persistence (Clerk): on sign-in, adopt the user's saved theme + * (single source across devices). If the user later changes the theme, mirror it to + * Clerk `unsafeMetadata.theme`. Guests have no Clerk → localStorage only (the store + * already writes it). Clerk is async; first paint is correct via localStorage, and once + * metadata arrives it is adopted ONCE (if it differs) → no flicker. */ + +import { useEffect, useRef } from "react"; +import { useUser } from "@clerk/clerk-react"; +import { useTheme, type ThemeMode } from "../state/theme"; + +function validMode(v: unknown): ThemeMode | undefined { + return v === "light" || v === "dark" || v === "system" ? v : undefined; +} + +export function ThemeController() { + const { isLoaded, isSignedIn, user } = useUser(); + const mode = useTheme((s) => s.mode); + const hydrate = useTheme((s) => s.hydrate); + const syncSystem = useTheme((s) => s.syncSystem); + const hydratedFor = useRef(null); + const lastWritten = useRef(null); + + // 1) OS preference changes (active only when mode==="system"). + useEffect(() => { + const mq = window.matchMedia("(prefers-color-scheme: dark)"); + const onChange = () => syncSystem(); + mq.addEventListener("change", onChange); + return () => mq.removeEventListener("change", onChange); + }, [syncSystem]); + + // 2a) On sign-in: adopt the user's saved theme (source across devices). + useEffect(() => { + if (!isLoaded || !isSignedIn || !user) return; + if (hydratedFor.current === user.id) return; + hydratedFor.current = user.id; + const saved = validMode(user.unsafeMetadata?.theme); + if (saved) { + lastWritten.current = saved; + hydrate(saved); + } else { + // No preference yet → seed the local choice into Clerk (so other devices match). + lastWritten.current = mode; + void user.update({ unsafeMetadata: { ...user.unsafeMetadata, theme: mode } }).catch(() => {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoaded, isSignedIn, user?.id]); + + // 2b) When signed in and the theme changes → mirror it to Clerk (across devices). + useEffect(() => { + if (!isLoaded || !isSignedIn || !user) return; + if (hydratedFor.current !== user.id) return; // don't write before the initial adoption settles + if (lastWritten.current === mode) return; // don't write back the adopted/written value (no loop) + lastWritten.current = mode; + void user.update({ unsafeMetadata: { ...user.unsafeMetadata, theme: mode } }).catch(() => {}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mode, isLoaded, isSignedIn]); + + return null; +} diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx new file mode 100644 index 0000000..7390b16 --- /dev/null +++ b/apps/web/src/app/providers.tsx @@ -0,0 +1,166 @@ +import { + QueryClient, + QueryClientProvider, + QueryCache, + MutationCache, +} from "@tanstack/react-query"; +import { ClerkProvider } from "@clerk/clerk-react"; +import type { ReactNode } from "react"; +import { toast } from "sonner"; +import { ApiError } from "../api/client"; +import { clearGuestToken, getGuestToken } from "../lib/guest"; +import { AnalyticsProvider } from "../lib/analytics"; +import { CLERK_PUBLISHABLE_KEY } from "../lib/env"; +import { useTheme } from "../state/theme"; +import { ThemeController } from "./ThemeController"; + +/** Skins Clerk to Solarch's look (feels like our own screens) — theme-aware. + * Applied to all SignIn/SignUp/UserButton/OrganizationSwitcher. */ +function buildClerkAppearance(dark: boolean) { + const cardBorder = dark ? "rgba(255,255,255,0.08)" : "rgba(15,15,14,0.08)"; + const popShadow = dark ? "0_8px_30px_rgba(0,0,0,0.6)" : "0_8px_30px_rgba(11,16,32,0.12)"; + return { + variables: { + colorPrimary: "#ff6b1a", + colorText: dark ? "#e3e3e7" : "#0f0f0e", + colorTextSecondary: dark ? "#a8abb3" : "#5b6675", + // In dark, DERIVED neutral text/icon/border should be LIGHT (default black → panel + // kept showing "black text"). This flips all of Clerk's neutral tones. + colorNeutral: dark ? "#ffffff" : "#0f0f0e", + // Text on orange primary button = black (both themes; never white on orange). + colorTextOnPrimaryBackground: "#141414", + colorBackground: dark ? "#20232b" : "#ffffff", + colorInputBackground: dark ? "#16181d" : "#fafaf7", + colorInputText: dark ? "#e3e3e7" : "#0f0f0e", + fontFamily: '"Satoshi", system-ui, sans-serif', + fontFamilyButtons: '"JetBrains Mono", ui-monospace, monospace', + borderRadius: "0.5rem", + }, + elements: { + card: `shadow-sm border border-[${cardBorder}]`, + userButtonPopoverCard: `border border-[${cardBorder}] shadow-[${popShadow}]`, + organizationSwitcherPopoverCard: `border border-[${cardBorder}] shadow-[${popShadow}]`, + formButtonPrimary: "font-mono", + headerSubtitle: "font-mono", + }, + } as const; +} + +const codeOf = (err: unknown): string | undefined => + err instanceof ApiError ? err.code : (err as { code?: string } | null)?.code; + +// Single redirect on 401/402; when concurrent queries throw 401, only one signOut/redirect fires. +let redirecting = false; + +/** Is the active client a guest? (no Clerk user + guest token present) */ +function isGuestClient(): boolean { + const clerk = (window as unknown as { Clerk?: { user?: unknown } }).Clerk; + return !clerk?.user && !!getGuestToken(); +} + +/** Auth/plan redirect. Runs on both query and mutation errors (no toast). + * Returns true if a redirect was performed. */ +function handleAuthRedirect(err: unknown): boolean { + const code = codeOf(err); + if (code === "ERR_UNAUTHORIZED") { + if (redirecting || window.location.pathname === "/sign-in") return true; + redirecting = true; + // Guest token invalid/expired → drop it, /start mints a new one + // (Clerk signOut unnecessary; no session anyway). LOOP BREAKER: a second + // guest 401 within a short window (401 even with a fresh token = persistent + // issue) falls through to sign-in instead of an endless /start↔reset loop. + if (isGuestClient()) { + clearGuestToken(); + let lastReset = 0; + try { + lastReset = Number(sessionStorage.getItem("solarch:guest-reset") ?? 0); + sessionStorage.setItem("solarch:guest-reset", String(Date.now())); + } catch { /* no storage */ } + window.location.assign(Date.now() - lastReset < 60_000 ? "/sign-in" : "/start"); + return true; + } + // Stale/expired session: also sign out of Clerk → client+backend stay in sync, + // clean sign-in screen appears (otherwise "already signed in" loop). + const clerk = (window as unknown as { Clerk?: { signOut?: () => Promise } }).Clerk; + if (clerk?.signOut) { + void clerk.signOut().finally(() => window.location.assign("/sign-in")); + } else { + window.location.assign("/sign-in"); + } + return true; + } + // All plan-permission denials redirect to /billing (including ERR_PLAN_CODEGEN; + // future ERR_PLAN_* codes are automatically covered). Codegen 402 (stale entitlement / + // race) falls through here; this is the contract promised by codegen.ts & CodegenPanel.tsx comments. + // For guests, a plan limit = sign-up CTA → /sign-up instead of /billing. + if (code?.startsWith("ERR_PLAN_")) { + if (isGuestClient()) { + window.location.assign("/sign-up"); + return true; + } + if (window.location.pathname !== "/billing") window.location.assign("/billing"); + return true; + } + return false; +} + +/** ONLY for mutation (write) errors: auth redirect + visible toast. + * Query (read) errors are NOT toasted (background refetches should not bother the user). */ +function handleMutationError(err: unknown) { + if (handleAuthRedirect(err)) return; + const code = codeOf(err); + // Codes silently handled by the component itself (prevents double-toast / unnecessary warnings). + if (code === "ERR_VERSION_CONFLICT" || code === "ERR_EDGE_DUPLICATE") return; + const message = err instanceof ApiError ? err.message : "An error occurred"; + const suggestion = err instanceof ApiError ? err.suggestion : undefined; + // Rules Engine denial (core value): message + suggestion description. + const isRule = code === "ERR_RULES_DENIED" || code === "ERR_NOT_WHITELISTED" || /^ERR_(00[1-7]|COND_00[12])$/.test(code ?? ""); + if (isRule) toast.error(message, { description: suggestion }); + else toast.error("An error occurred", { description: message }); +} + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 10_000, + refetchOnWindowFocus: false, + // Don't retry on auth/not-found (avoids unnecessary 3x + redirect delay/noise). + retry: (count, error) => { + const c = codeOf(error); + if ( + c === "ERR_UNAUTHORIZED" || + c === "ERR_NODE_NOT_FOUND" || + c === "ERR_PROJECT_NOT_FOUND" || + c === "ERR_PROJECT_FORBIDDEN" + ) + return false; + return count < 2; + }, + }, + }, + // Query errors: auth/plan redirect only (no toast). Mutation: redirect + toast. + queryCache: new QueryCache({ onError: handleAuthRedirect }), + mutationCache: new MutationCache({ onError: handleMutationError }), +}); + +export function AppProviders({ children }: { children: ReactNode }) { + const resolved = useTheme((s) => s.resolved); + return ( + + + + + {children} + + + + ); +} diff --git a/apps/web/src/app/route-guards.tsx b/apps/web/src/app/route-guards.tsx new file mode 100644 index 0000000..831daed --- /dev/null +++ b/apps/web/src/app/route-guards.tsx @@ -0,0 +1,62 @@ +import { useRouteError, Link, useLocation } from "react-router-dom"; +import { SignedIn, SignedOut, RedirectToSignIn, RedirectToTasks } from "@clerk/clerk-react"; +import { useEffect, useState, type ReactNode } from "react"; +import { ensureGuestToken, getGuestToken } from "../lib/guest"; + +/** Pages open only to registered users — not accessible with a guest ticket. */ +const AUTH_ONLY_PATHS = new Set(["/billing"]); + +/** Signed-in → app. Signed-out → get a guest ticket and continue as a + * single-project trial (lead flow); if no ticket (guest mode off) Clerk sign-in. */ +export function RequireAuth({ children }: { children: ReactNode }) { + return ( + <> + + + {children} + + + {children} + + + ); +} + +/** Visitor without login: guarantee the ticket, then open the app as a guest. */ +function GuestGate({ children }: { children: ReactNode }) { + const { pathname } = useLocation(); + const [state, setState] = useState<"loading" | "guest" | "redirect">( + getGuestToken() ? "guest" : "loading", + ); + + useEffect(() => { + if (state !== "loading") return; + let cancelled = false; + void ensureGuestToken().then((token) => { + if (!cancelled) setState(token ? "guest" : "redirect"); + }); + return () => { + cancelled = true; + }; + }, [state]); + + if (AUTH_ONLY_PATHS.has(pathname)) return ; + if (state === "redirect") return ; + if (state === "loading") return null; // ticket issuance ~one request, instant + return <>{children}; +} + +/** Minimal route error screen — prevents a single render error from crashing the whole app. */ +export function RouteError() { + const error = useRouteError(); + const message = error instanceof Error ? error.message : "Unknown error"; + return ( +
+
// something went wrong
+
{message}
+ location.reload()}> + reload + +
+ ); +} diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx new file mode 100644 index 0000000..090be0f --- /dev/null +++ b/apps/web/src/app/router.tsx @@ -0,0 +1,48 @@ +import { createBrowserRouter, redirect } from "react-router-dom"; +import { AppShell } from "./AppShell"; +import { Welcome } from "../features/welcome/Welcome"; +import { ProjectPage } from "../features/canvas/ProjectPage"; +import { RequireAuth, RouteError } from "./route-guards"; +import { SignInPage } from "../features/auth/SignInPage"; +import { SignUpPage } from "../features/auth/SignUpPage"; +import { ForgotPasswordPage } from "../features/auth/ForgotPasswordPage"; +import { SsoCallbackPage } from "../features/auth/SsoCallbackPage"; +import { BillingPage } from "../features/billing/BillingPage"; +import { SettingsPage } from "../features/settings/SettingsPage"; +import { RefundPage } from "../features/legal/RefundPage"; +import { TermsPage } from "../features/legal/TermsPage"; +import { PrivacyPage } from "../features/legal/PrivacyPage"; + +export const router = createBrowserRouter([ + { + // Root error boundary: show a branded screen on ANY route render error + // (incl. auth/legal pages), not React Router's default dev page. + errorElement: , + children: [ + { path: "/sign-in", element: }, + { path: "/sign-up", element: }, + { path: "/forgot-password", element: }, + { path: "/sso-callback", element: }, + // Legal — public (no login required; Polar domain review + user access). + { path: "/refund", element: }, + { path: "/terms", element: }, + { path: "/privacy", element: }, + { path: "/", loader: () => redirect("/start") }, + { + element: ( + + + + ), + errorElement: , + children: [ + { path: "/start", element: }, + { path: "/billing", element: }, + { path: "/settings", element: }, + { path: "/p/:projectId", element: }, + { path: "/p/:projectId/:tabId", element: }, + ], + }, + ], + }, +]); diff --git a/apps/web/src/assets/hero.png b/apps/web/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 zcmV+cGycqpP)V|)f$;Qooc7=_G zlYe)HToTQIc!$)^+J1M1y0*T%w!p~7%ux`!eRhO?c80XDxKQ*R^lUUMnA>6NT^?feoZ8xxvP32D&s-9ow zqjcM}eesrC)NeDmsf)*P7wJ|K!&xP%Zy4iI8lF)Tv2!reW)tCzg_1=PmOwd1SQfxa z8;58t!=z~Ba7CYlNWVG>he8aRPY|+-JmozNhn!#9i#77Aa_Edt$ijyCWL#=~I>~2X zZNrQ8I0=D+NWD4pq=7~(i zhfThMNw|G>g^y9pGzxX7ZSApl@tIxFcs{p#MX{Ax&XZT+cR#U+OWc@S)pkIuI}dzu zH?^Q=<(y&Vq-oxSLfc0Zmq81bjZWf}RnssBaD6}2g-XJHLcN_|*IOu>m|x$nbm(?E zyNy!Zp=RroS;?Vg*kmoJYBi!n5{_^@rA!)=t#a^;N$8GL!*DsQb}`yvEuX!G@||An znOfUZAevPrkV_qjl|<~3QRZzG&h@C9Y5z zqpNH4xqbF_InIPh)kX}Vn^5kyed|mOuq+2>M;v~KO37a#yrEn3XDqtOl=rc6_KZ!; zreo)DFVB4|>1Zd(bvMI%8uM;3!)YMYu&cG?(PE!B~y@3yKBMt|R zAf=I16tFwPsl)!jDqvYkLHaAQ+f@W1m6F5aZvwhm4JL z{_l)@b;)mDSzle2gyFP5-r1x-5X{G}ot%VyWP@vEW80!Q=f%RTfpg>B*TA^pyWYUQ z<=xPtz}WcZ!;rFl4m1D&FFHv?K~#9!?A%+fn=lXt;9!Fc#kQ;zk~gZFsH z8e5iu@c_pzX&qb8&Dum*oXwB+fm6l6gFfC|o*wgEiy6tw~&co z9Vd_4)P%wP-KwQW7|lN-znGK#?N+j24U=$982myIBM+vsiKsc*@4-rwJxuAaHKna6 zT3wi!C~a4ZKH03qU}_1bKyx0&$CaK7_%Z+Kl$)fF5^op zZApQF2TvDav!s|krTjw-8US6ep z%!VmX4luub+fseQz_D9ATJQ?iQQwD}TZz{-yo#l12a%+7bT@E(X-hyaVS-5vuXc#^ zx^w;L21;NphGVoj*{s3f4dme0y2LC=G1-7THd`#z?;tuC{^9k(dM{Rf2GOxg7Jzho z7nSZHl7?M9kdalX`)YgoKEfiae5+;$(OGeN1eqxrv!ZCVKyH>xiyNqfe8xzY8*7)H zQls8KMp)F4D>ED;idMOU^^WhVF@q>ZSmeB0y~qC~|DB648hr%Sh|*T(4q|w2l?m2+ zvBVw3@7+Mz?^Yc#+se6KM;a<=(W-I>k)$-qL2V*t}VaW`;?P4)WqI%maIDq8!oUcSYAD`}wWjkSyAVsnF65#2zQ zZ>(K*TlS(E#4y$4Zq+e^_&}d)q20hCe3!LfLYP%nQpLJ~gM6a1hJlz3)aS<9C9me| zAcmJ#>tOwBy{HoP0Sm1&_(E+S@6 zgBIFUoei8zJmdpiq8q5=OY7t@`)JWxn_&GvKVr=Zdb_pEL_j|=?f;WK^U9Q0efd#K z9q7SfJTl4pmA$jsZ5oK8@O9#!I3Cv-kL)<8SalSsp#dcpvJ}Nz#G6FC0%9|7Fi#8; zGDJXtj!&GljT3*HE@0EE>G8Se&d)*nkqe}-?`3vPl&UqK?xG z!3XJ4M-x`EuQjhBbu?ik-)rmIt=DF_N?TVMP)8Gjn)TZ2V%H|zENbeix}kOxd@0}Q z>)HuH6Ean!uS#~4g2Ne2WsMGel|h%j9*W_quQheG^JqmKhc*RYzp0wKlGjBq2VzY_ zgOv8WC1+%W=W)k)Yp_`8kfE=uiiwOZTXi8Uj9YGr$f@yJcJ;#&-Nq~sJ7anE(@;QN z=~br%7%7`isKStX|7!1?L(apl^QvPKlrHV4S+6tNVQ*R1iGdC~WMNE1$a+=rpQmcB z>wxiLIBvOnm;u*;9Y!kJdy(T4lk|8>JAm(&wEsFIF1$_*{>2ZNd$V6DS=SfrGxAv0 zzKe377JI`&o9Ljr+VnS*EwehA{f&{cKZF(6*MG5!p5MvrFA3ll{fmRG*L@6^cb;o^ z3Wm8c?Sc6$`>~VEWw(c$Y?nRO;2Q$=ulpqPtM^=1IZx;@xK0PgO7rKQ^WHVLwtgUT z%|JF{^f(VH)wLKQ%dYiu2RmchBdxL0-M?wxxul_z*{h6ZZ`>-k(vizs((vW8Lt6Z6 zY;Dt?@JWyN`O`f;&d1Mb?e%9oyRK1ql?EE5XB2(W)|D1~Rx35$H6@6)$F?)7V|zEO zI}fu0-0}8W5=6sg$fPnZ~7=tTudl?Ecb@pxbo)vni%gP-?hL|%*?62C;x6?@E`VRnJv z?fTb;k4x;TS7Cu-z%J}uy}e-pwpLQ17Q@4DC+FCdAmNKklG$`I_pyw7E{fYmw~{Fj zi?6KcVy=Wrel)EB_DWO|0CKmI|13!gBV?X`Ozp7x>?6jr`>Qz=^4ea35!$*f}) zS$i+x_k+@P2q1RFUH^ZTTk7=n?cjfR>hTq3l3SY~#w+I8SSutXGyhw;Ws~=zMQ%Vc z>$On~47Ut?P*_!TOQ&PFmLAyJieB2X4_Fd_!WxI-AY`q1Lc-oK?+qcOTzlQ?@~x@OT}*9jTVNfl@3rGvZpWI=eKg>T zZb@6YWz)J=IhP7CF|c?G62vMEG%#U}?#86$0jR4sG~i(jRd#jmn`7b(O#?N;3a;1t zhXLssmUwGhp79luw#(*V8WL0|8+E z6=YZ_O@er~$LrD_PYGc(kJgB=;yw#+Z3X6LDUZ(NcwN=B-hjdiHm!JFar%m{(5bEW z@@_VEtG$5;`EJZ|OkJ@l&G9n((w@uNFwmU%bG|s#TbcJJos!{e+bjCjrCq_}LcN!UFgKtgg7siV*7# z!}1whTRRi*-avJPu->C}Z8EiuK$#886+H_#_!btv+rsiBbv2jAJvJ+O0{#}y(%L3H zfjU-kq_-L@2XrL*ae{{qYJkD{@dw%*bkh2P&YS-0!Xt!PRz7KHV0+~j(t9W8lAVWR zt@B*DgURgEz4>WuN>o?_iKcw$?k{||Pg7{Q2o4|VmJ)mg?{VQJA<}zEr^YAAS zgGm5RT4T3p)U;yz-tfBO^kw8?IoG!IVmc+Z3m#}AOQ?5MRa>)OcU!$N^_+yK6ayn? zK>~WK0!#ysuj^oNLakm)Zvu+J)OSubX^kv!c*xgdIvs;kln!rgG4*uZ;w0mQQO4XD zO9P{GNdv!=cQ(CAL{S(%KtuV^zC&Q{%g)PoXnp^gn^>c*`E>$hLYg2HjnbVGtWLa{7zHdG1jT@B{|Dm16 z7K2(jsfG+m*Zxof)iXxu+!H5Mo-0$pkyV3VV4B@Qms46M zuBxGRV@HxU7Wwx-6CB zaU*HO<_qn$5GH>&@?nRy1{z zkik!sLfWQ)r#75)vVwCBU*r_)Q6mp?!j85{#Xqse)ApRdE$V0%I0*~e(_{)5H)`Mk z#rExC>yjhZxuL@|+#v4#<Axw$+VpV zuT;!2Vww$je$DpAW`$FX_Ab|Ip%$;&T$-lW8jS~B$>G}rd>eQG+$h9lQx4Mx0w={m zx9?T6VU`>sR}XClkAhHEShOUe8awiq zmizhL+}5UKs3}6~It7vBTig9dfQ2Q8coo+Miiaw7n~>4ybv2Ptt0^^=VqX(t*Yya9 zr`FxxFX8(v*H=+uJ#JJWIB2A(==HDYx~^zZ2nu?2`}|Wsa*f3h3ixc+U|FDtAG$Y! z*lc_7se5Oso-Cgqe0){{!8H4g$3<8!R<6JOurD;((({c$1(pwb>(#TT!sge@4>r2@ zVL7>U`0`nsWAYErezk4(Z!gMI2?UTo{J3Ajo(u4)KYIRd>BRcG4BoS3G0EXyEp@tw z%P7__?A^a>Q&AKL@ayDO9D*Qkc!NHnO9l}kpp_6hXbMppYL(X1L?njdFT|-h2<_$; zAtDZ!1Rf%|yb!qbWKd}%0b`LzBeyNy43|QO(&h2mxQLUL)|0%agVOW)6TV!&Ip^Ls z`PG2cygM8)IecQx=Fc+nqYRo4hS^^-nM_&-y8?EJXUczP=DIw(GkTJdpEdh<_STs{ z|A)4n1GKdE=Wu!!nYoZHcUQ4S&R;oDOKX2lrkdF(mK>hz<$Pp>igjOcvoRIjlN=W8 zu8Gx5(roqn8$>gEE5vy{GiGeW8Tq{vnf3hS-V=$tZkQuftUVuU8o6k&dn=Yg3)6MOIH>nlK^-2+C6BZITr~1@So?NvG#TwL)|~=1YXGMTLpS<)ziK_CSOabe z=cB#5)yz|@0i9dSo?*CX)}UP=s6)B+F@~Em(u@Q(I9J9i_V{LmMu8BfXYMh~*oPP+ z!3~xTv|(>|=n6ZOtT~C@V!z!w%18*8T2t6}U2S##rC)mekBql&VsBX;$~ByGE$oA9 z`0Wzq8p?R{4)$l*on;!cLa}Dh^Xe?owiQZt9nH1fxxh$pN9K%CtOw?u3>85L7rr!d zXs)l{TZ{xXP&U8exz?9cv~dNNibOmt*K4I$?RxqIBZ0(?Mg-9FS{*9Bc49Qc1`=sIF-rye`aNT1G@4NwXcnyc@+bw_mTsR>5< zF<2;X0QesG_pw|TonqVBhRtfqI>ty(SIu&VOXd0CrLlfp+;WH7HYjhqnu^oAY!9cB z=B6#R?Rfz9BP`dJ=@v_?70s3HxQPk+{6Y+lM85f2NF^00*^OcM0~?JOZfR9ZPYF+# zYSs}(_BUYV8{n@2a1hD^SV41bwmi2uztR;PeBgF1F-`9>`zoNss-@3LaF2sjl~>OaaVmp7PNp+UT`6@}gR%uzqHDVeEZ14{Yt?n%JeQm+t(1_u zSc}oj^{b;+rlS|ME%+LjzSI&xu0Bblxo$MJ-J$kJ?Qu_XUXh}*@*-x@ny|}wVM%Lg z3tNB`yvr*}N?ClGL;H2cglcvErIccU3(eP7>@~4nOIcI~-`P8tSQnx=jI&{9)!1}l z;gQ%_h>ZlPSV@o@Azq1R$C6ja5!^ZGh;YRhhxs58qJWo9@Bceac&yy(pET1hnn`~7@}2L0&dfPKYs$ih7m2}R!25!(hxqA(!UIw; zK4+~Jowy3=RNC6nE=ncU{LH5?*9@W24lacJlvCZXB$CYtE@>c+~H zkV=(5I&gb{xn2!~f&fs2NQgAL6`p|kyt6kpWk}iVlqIp(H;ig`{_U9yxs1jzu^ETM z7~)Rg8C-NueqTYP&U8l{DY=Y47cR zOR@U%$KQV{mkRF|4)z9Y^t3K`@p>duY&QLUFeh6VoV`a`$U@)(z!-N*5Cj<11$EZW&hJLX83TO{lJYP74rlDZQPkm@t<=U^I)x@|UnHHkdQlh?!ltZwl92rE;;^ zZuIappj4dhld1}kttYYV-j|KF1Kus zWBnzttD^00%LFK(wrwNragFub6xiV8QE2rm<`&fcR4SLFcdtLxVuN!Aal-g6dE4%k zARZ}|xeo;K{0yf7@9aua%2j5o)CPcIOc6uLHFJOcgtB5owlcNAwyAHc0QB0Dts?c@ zUemG~j_E&W7R%+x-IO4FJl8e&*2Blmp1S#RA|)geVrxvP)NHdYuxi~g&Etn?QdNK8ZDKZ?QFLU?zh30G|t9G>a_X4zk}Ygw<^$7K!GIn(Io$>(d4ODJQ2XSd%jpK zm7>ptl$a3GyB}5-%p4>Q*p#VL^B{yQMuFCM^#l#+N!Ne z5_PrJWB=@Iy+t)H`g1lX`{bm($KE5I?0c(JEYm#t{F}j!xtsbob0{xu@0TB_*>G7w0ICn zr#VoBktqHZ~XxhiKD*lcG|b;H*|Ny3P^8ceV`sfBRfrhwZ!T+MFZ!F1Bt{q$8d9i6o?~ zODj^POr}&ivSa^R^YFIq7o0giLBKCycH_aU`F6)O6JX%nPTwh~Q`eq6*0iE#Srj2^ z*_hN3%*b83zfafy60@Cp3{J({RlSaEn&E?mrxRNC9GQ7#+f=s! z0KBf-9Ny_v2VbE%aB|Di)5kNJ^t&C`4D(>t7zYUWUFtbxt+Oq=!@O7BU)}>d*R72o zFF)3jQD_lLe4is&xzyJYC1-c{8TX$RU>&>P$%)ufpez0XSAukmh!xcekg`s$c<>-q zI#zn^JU0zzF}V60)o$_gY}PQH>b2M9&8fRZa#OauglPb zeQ@pMm&=!vNgos4CluQjLMV!pfkmxK+35bi^k&=k>9h02?l+u+m0agG;(h2|Jslc-llvtEwn~*w3bx7qnvZACG<8}AGeaDVvcHbKd2>3G^ zSFPULUn-?Pmo^-_`mLZr??uNH`2=I&yajlrF{DtUxMy#Nu}z=3y7qbUA;5`)hibMR zhXL@@uKyV0-2&A@t@!xyrBnMJl&^o@Gx$&5_q6?D=ji5grd-~=?dlg;ur(_V0wjh! zA=JV^C1m+DDkOsgr<%O9ZQFg!0}pD(#PSz4Dr_EyS5$`)VIAv);4n-SFP~YtC7sH= z7&*MfpH;gd*FHbkmD#)hVxb6xjc9~`t?_{=JS+@ip_cTicXxG<=7m9& zPX+Z8IC*GSAXuGCrZDHgR$r%jyk-fctis2Kx4HvZ|B~8uC@o)m^>Hy-O!&TKA?$&n zkP2Xc54w~!=z2?^NafyL*L0V9cbYrugHBBUj`xVyZmGFR&kvk#>1J*Z~i zNTz}?IAdJ$gkqd2!Gw(%LzE!O5s4C7q4%T~e_P{+z=DNDKrG**p=U`d5yg^vp`;Zn zsU=8gd0a9s4s0FPJePWR9eH5=+O^Kks&kC-iblNqTh2&Pw*^(4384f+D8N|fewZu_ zg2ejQ)ov;ztz;NQl7yj;A`(!H!XQu_$sqY9h_IrH*}_%1{L&_YLDvO?%R5Z-t+ClW z_qERbL?HKUZ!nt+!E9S`uoh^5A|DaIHe*_gf1`E_Vq+}{&T@t$EGhMnRjJ4z2w_W8 zp+qjs7as22^&S3wY1?+}^j-I=RcCE>#|39)g(lU7v_8;?=qK(9D8-*pPdiy)P3lIblG`+?%ea| zYoD3dopYt!tKgFicfNmNi(EWE=E4hC6(r|PYtanqJlmt57YOVrr2^tfrG(eG9C##X zu&1t@%L$RIvpj!wUA z8i>Pqot#_+Cnp6L2XPcZy1ar|9MnY+7eNvK1E)@Tr#2KsXq1*>)uUCozT7L##ok?o zhA6ofP4E|b*9tAfG?uf$#}>TIR&1A!yslP8}i7w-EzW(x#9VEvx18k%Tn=-$VV zkOtUr0b2!w3t>h?#8AZl^Az*(6KCGlD;4j~yx};`#2gN1_gv=%7KVzecIRakN{f*4 zeaI>yH;-o4OGhvGTU)(quWI)-q?V*(sVesSMv|wMUQ3hLEt=lBB$KZ9TyHr>)f7o%) zPYeU<3P)*P10*7vE)nA5#{c=6-E-_>r_u4e3i!I2+UksELwDqwMeBZ9FSP$;^Ajro z_@M#_Ss$?ejoB@!wN|kbGKs(0zLo%0QpQXW#t;oC$B0MZYZ&Ej?8~fNhcCVvPo3vo zFn0WWZaPliF^8_}yzb`*f@yg0uWv6HgNI)xa=pO%Ck(C<=-60l#uD3(wXP~c7!NoX z0&^6=N`zcc90F#qt@=Rn@r!3(*1v(Tl{B!m?Mc7yIA+nEHpY{YWr$=)F7rhR1P}(v zt{YhY#;jsW6G>#xhP*B`OCk|Pf+NN;ju1rxa*HAgoGq*rvqw&xe~;t1JA31$s?GBb z*g7&@cbKo4n<`>)!UlIAgR6q&))B0KYU8r66GbFj?8Guw4E%&}Qi_lT003LtoIZei zwD~=XZmeo+yZ2Pq3KYCF-R&11^p= z@H%s+=G`}wrbJ{()Mh71#2SP3Zy3m>l1n?0N-N1Q;z6?oSxr-G(H5m4EO>~&;}VKi zfY}3w+9z>vp#d)hVuu`)vG_aaH%3b=WKMnSu&c31;<3O;bz2iD=w+o4#oBb36 z5ZCF*Gu?zjZIR0S>_%pHY2$k8D^n7Sz_K8tCDeXM+dO<#LSg%h6`~dnVG1N@T7v&e z%wEd1!k{^zfz_1BTW{!$!B%g)J^2b87!9Y>>100X1SgT7s0z$o>^lAA=Gp_cC1(h=*5Tmf8z&LGJJ>$|K^~s`z9*OWz5MFUr?>Bi?_PGBB)#psD5?>n+q{o_ zz7~ez&;t#h8l$jwGPCC&xq2YetXYQT+0F3j(`xmNGf8dj#an|p#I*pvI*kwW4iuB> z+q3_7xB8y;pLzHG-S%+UHQA zvqp;$kmGJY>lLsN4C~&TcvAS1SErTcwcw0r@wngk zShAUA1M9b#g}^pL-zH7Q#z^&j#r9F8BTVfkR&qF<=e35goTu7c|GN)0mokj4m0%~0 zXJ8j4Hc_l;HJ&uU*Iw`8d_EscJ``s0tk9mkKo^&#TYXm-EoAzTQObxa@^u~g2t#T) zJz|rE!I_?i4dCJC=B8(_pZ{YR>|V?0iCcnU;E@$239^x?SYCfNaMHN;CtHIS_zHN9 zTkQc1v@O35okiFtq5_u+5FkY55ap@pi)O?}x0D1c*qB0KpYR}>Ul+B0Vmr}Z@+%mJ|As}sis_=ROPbov@*2thpE&?!V#Qgu$snYvCZ zrkhmkMU+fSf-s8(L37fPr&M*jRs{{THb!aXQu|P9l_-vJhHvLzMGH zE?1U0H_+PmNABp9`|KzkGfrrZ%XvdGo6*<{d5m9~L7 z_^`M;X6xDo=m6LY6RfvJEvsTK1!u8d2HPx|$S}p;sRy!I zWL55Yxu~_B`OP@~(q6&W3#)~I&+MGL%GWR$#udC151^wsswhqlii;rP9jJpiI7o&Z zAb})=HY7?4HA|re3ns`%$)FuvKCFWjhb~?IE)F6dF2K5}poj-NK6Gf;hw$t3=1txY zoxQxZWrQU6K!%|~!m?~Bnw-6Rr!F3BZ{u5!LqnZTDON}Coj9^@&le)V!NYrVwS~B% zEL+>Sr@}qGwGvu|HrOo|gSt__ezN^&%~{*)a=rf7y1HujUcr`zZB<4#l@T#eN)si} z)lZA<{=tKx8E%c9>A(##6}_p+~EZpKsl5a4pj`E*;_-6`ysiv zffA!7=MT1vCz}-m4~tjVey1b2KSR4OEtLd-(_DdUqYZ74LaDkhH?KFh?%WAOP2WbX zp@zT+Dx|5_f%JQiAGvVw!oh+g3e50u!aPfMxdC=E)XB{F5IcEZhePIM- zph6Y`$Oy?JBL<8Ex(SqEhLeQ@XcrdA>a?rx+_~HLA;l14)WmmpH}_w?Pg#HBZs0eS zwypwAW?M-x+3AU-(GGWSJ=ngxUEcEZ5OsX(Qlt!MQ zn^(`S{GHkAv(8@D`EAfSYig%Cxv?z!{=w^F#y)5_d7FuKZH7qlR-#5B0bt806%D0I zT7VdVP_?q*%Rq8UR;JkD4i^RXowt+E%#V2U>TfDqzZSDZ+dR!a#T3I>-z_$q9@k|m zy5~A*m~&JWP@E7a=pc}4kVHTc4h&R;Li7d@f`|hKMLkbb^uhOakNr3&FLjlm~i5NBM< zFaYI{;cpiHCNRdE0dg*>qIm(_t?#$h=(SCw?h3rJV2*ER8{O4^3#=dO)KwklZkoqU zS8i5c%YL*y*4;FY#D=XmkQnYj%LH)?02~gSJH`Qp1XY64g>%c_K$xseI&|e)7vRoL zAqRba$G@%fSGA7X7hQk%_3NVOYVS+$leU_!&6*5uN)8#5ZBz_6ASCA;azYS-Rt@ki zg2NWz(=;t}SC(~Ibl63$5C8FPmhXqb^)5#jaJ~I{Ex3xZ!+2h8$}}h_g@Be>HZ;72 z6#y#>AY3^skuVKF#0WxFBQ()5d5_nWb?c6c>EeMM|Mh+*&wEpPyxHCq{R-Gdr-`hN zF=1sxl&mBoK+#qRLl9#CEN|Fg8>nbmsTg3a1;#M9enQ$RgWk}kp#-5wh=EF&1tl%mJln2V^8o%Qv(*=zEuO7y z=m*8?xpUn-*@h5Cl_3BK3joiGkyaScK+>|MWdMRWm@RT!Q1piAlv5hL@B6>3&GI8) zP!xBc6}ZNIpJLL%2a8Y!+(<=f%WX>_uWVxlga9!D*oYt$l0cxRDMvqfU;Kq_mLK5k z)dvqYcgLa_Lz?3HyeF)@$%$&6lI?r4I>6W#M*<)vq{?&Oqrx``d`mhpVPr> z#q078F6gw_X<=?KR>8%^t%@wbITvNMu!hKiTSkCTJkw>1!e*Y{%31#_yMf=LW7{RJ zYoC^w$6%3cBtVG5)x#{Hg6IVTh9XEcM{gQwXk!R^y95^f-hZ`d{aVa+xW1EO4wDV4 zB?JgD7*?qkvc|$nIykTvNl2x0j3Q!MXoLL^)~}d7jcYf(H8D~c+?$pKL(px>Z3`eb z04RzS6_AgFT6Pn#iZAg$Sl_j8#;6ShF%&(Fag#E2asU@@LaN;=b=Wf7sgPKhfzhBM zC@eFL8^MrnA*9&Khe*Ab@CC9*uyJGXyi(;y2>lQLJZt;ShtJi?3Yf_t`F+$hY!+Q2Ndsx=U+bjTiAy7djLji>7k%k`$9&--f<*BNA3Hy&ZrHH|4 zG5H&9cB?O#zI1_OOf0Ce%mDfQxdtp3vU%(iY6yji3iISS61XLv#z|!zI_sZqza@B+ zyu9st5-h+`H7QUKx9}3w@oU@EO}&cEzG?fu!!bLO->%zkcg;i9^j`S~=WKMnDi1f= P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/apps/web/src/assets/react.svg b/apps/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/assets/vite.svg b/apps/web/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/apps/web/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/apps/web/src/canvas/CanvasA11yMirror.tsx b/apps/web/src/canvas/CanvasA11yMirror.tsx new file mode 100644 index 0000000..842d5b2 --- /dev/null +++ b/apps/web/src/canvas/CanvasA11yMirror.tsx @@ -0,0 +1,89 @@ +import { useRef } from "react"; +import type { TabGraphData } from "../api/tabs"; +import { nameOf, familyOf } from "./families"; + +/** INVISIBLE DOM mirror of the canvas for screen readers + keyboard. + * + * Canvas pixels never reach the accessibility tree (WCAG) → the drawn graph is + * fully invisible to a screen reader. This list makes every node and its + * connections readable. Roving tabindex navigates with arrow keys; focusing an + * item makes the canvas select that node + pan the camera to it (visual ↔ a11y + * sync); Enter/Space opens the editor. + * + * Visually hidden (sr-only) but focusable + announced — the canvas visual stays + * as-is for the sighted user. */ +export function CanvasA11yMirror({ + graph, + selectedId, + onActivate, + onOpen, +}: { + graph: TabGraphData | undefined; + selectedId: string | null; + onActivate: (id: string) => void; + onOpen: (id: string) => void; +}) { + const listRef = useRef(null); + const nodes = graph?.nodes ?? []; + const edges = graph?.edges ?? []; + + if (!nodes.length) return null; + + // Connection blurbs: outgoing/incoming neighbors per node (name + edge kind). + const nameById = new Map(nodes.map((n) => [n.id, nameOf(n.properties)])); + const out = new Map(); + const inc = new Map(); + for (const e of edges) { + const tn = nameById.get(e.targetNodeId); + const sn = nameById.get(e.sourceNodeId); + if (tn) { const a = out.get(e.sourceNodeId) ?? []; a.push(`${e.kind} → ${tn}`); out.set(e.sourceNodeId, a); } + if (sn) { const a = inc.get(e.targetNodeId) ?? []; a.push(`${sn} (${e.kind})`); inc.set(e.targetNodeId, a); } + } + + const focusAt = (idx: number) => { + listRef.current?.querySelectorAll("button[data-node]")[idx]?.focus(); + }; + const onKey = (e: React.KeyboardEvent, i: number) => { + const last = nodes.length - 1; + if (e.key === "ArrowDown" || e.key === "ArrowRight") { e.preventDefault(); focusAt(i === last ? 0 : i + 1); } + else if (e.key === "ArrowUp" || e.key === "ArrowLeft") { e.preventDefault(); focusAt(i === 0 ? last : i - 1); } + else if (e.key === "Home") { e.preventDefault(); focusAt(0); } + else if (e.key === "End") { e.preventDefault(); focusAt(last); } + }; + + // Roving tabindex: selected node (or first) tabindex 0, rest -1 — single Tab stop. + const activeIdx = Math.max(0, nodes.findIndex((n) => n.id === selectedId)); + + return ( +
    + {nodes.map((n, i) => { + const nm = nameOf(n.properties); + const fam = familyOf(n.type); + const outs = out.get(n.id); + const incs = inc.get(n.id); + const conn = + [outs?.length ? `Connects to: ${outs.join(", ")}` : "", incs?.length ? `Incoming: ${incs.join(", ")}` : ""] + .filter(Boolean).join(". ") || "No connections"; + return ( +
  • + +
  • + ); + })} +
+ ); +} diff --git a/apps/web/src/canvas/CanvasView.css b/apps/web/src/canvas/CanvasView.css new file mode 100644 index 0000000..2ecf4b4 --- /dev/null +++ b/apps/web/src/canvas/CanvasView.css @@ -0,0 +1,58 @@ +.cv-wrap { position: absolute; inset: 0; } +.cv-canvas { display: block; width: 100%; height: 100%; cursor: grab; touch-action: none; } + +.cv-hud { + position: absolute; + left: 12px; + bottom: 12px; + font-size: 12px; + color: var(--ink-soft); + background: var(--glass-bg); + border: 1px solid var(--hairline); + border-radius: 6px; + padding: 4px 9px; + backdrop-filter: blur(6px); + pointer-events: none; +} + +.cv-controls { + position: absolute; + right: 12px; + bottom: 12px; + display: flex; + flex-direction: row; + align-items: center; + gap: 1px; + padding: 4px; + background: var(--glass-bg-strong); + border: 1px solid var(--hairline); + border-radius: 10px; + box-shadow: var(--shadow-card); + backdrop-filter: blur(16px) saturate(1.8); +} + +.cv-btn { + width: 32px; + height: 32px; + border: 0; + background: transparent; + color: var(--ink-soft); + border-radius: 7px; + font-size: 14px; + line-height: 1; + cursor: pointer; + display: grid; + place-items: center; + transition: background 0.1s, color 0.1s; +} +.cv-btn:hover { background: var(--hairline); color: var(--ink); } +.cv-btn:disabled { opacity: 0.35; cursor: not-allowed; } +.cv-btn.is-active { background: var(--accent); color: #000; } +.cv-btn.is-active:hover { background: var(--accent); } + +.cv-sep { + width: 1px; + align-self: stretch; + background: var(--hairline); + margin: 3px 2px; +} diff --git a/apps/web/src/canvas/CanvasView.tsx b/apps/web/src/canvas/CanvasView.tsx new file mode 100644 index 0000000..86dd802 --- /dev/null +++ b/apps/web/src/canvas/CanvasView.tsx @@ -0,0 +1,1270 @@ +import { useEffect, useRef } from "react"; +import type { TabGraphData } from "../api/tabs"; +import { drawScene, drawPendingEdge, PORT_R, BEND_HANDLE_R, STUB_LEN_WORLD, HOP_MIN_ZOOM, elbowGeom, portOf, nodeDisplayH, type EdgeHop } from "./renderer"; +import { NODE_H, ANIM_NODE_POP_MS, ANIM_EDGE_FADE_MS, ANIM_EDGE_DELAY_MS, ANIM_LERP_FACTOR, FOCUS_FADE_FACTOR, type Scene, type SceneNode, type SceneEdge, type Viewport, type FocusSet } from "./types"; +import { nameOf, familyOf } from "./families"; +import { nodeDefaultW } from "./node-templates"; +import { arrangeNodes } from "./arrange"; +import { autoBendRatio } from "./edge-router"; +import { computeBundles, computeCorridors, type Bundles } from "./edge-bundling"; +import { computeEdgeHops } from "./edge-hops"; +import { isAiActive } from "../api/ai"; +import { useUiPrefs } from "../state/ui-prefs"; +import { useCanvasState } from "../state/canvas-state"; +import { useSelection } from "../state/selection"; +import { useHistory } from "../state/history"; +import { usePendingProposal } from "../state/pending-proposal"; +import { useCanvasCommands } from "./canvas-commands"; +import { hapticTap, hapticConfirm } from "../lib/haptics"; +import { useTouchMode } from "../hooks/useTouchMode"; +import { CanvasA11yMirror } from "./CanvasA11yMirror"; +import "./CanvasView.css"; + +/** Diff-aware buildScene — for AI streaming + manual mutation. + * - New node/edge: enterStart=now → triggers pop animation + * - Existing node: position preserved (drag/optimistic update not broken), + * properties/name are refreshed. + * If prefersReducedMotion, pop animations are disabled (enterStart=undefined). */ +interface BuildResult { + scene: Scene; + newNodeIds: string[]; + newEdgeIds: string[]; + /** Existing nodes whose version increased (content edited) → "edited" pulse. */ + editedNodeIds: string[]; +} + +function buildScene(graph: TabGraphData, prev: Scene | null, now: number, reducedMotion: boolean): BuildResult { + const prevIdx = prev?.index; + const prevEdgeIdx = prev ? new Map(prev.edges.map((e) => [e.id, e])) : null; + const newNodeIds: string[] = []; + const newEdgeIds: string[] = []; + const editedNodeIds: string[] = []; + + const nodes: SceneNode[] = graph.nodes.map((m) => { + const old = prevIdx?.get(m.id); + if (old) { + // Existing node — current pos & enterStart preserved (drag/anim continues), + // only content fields refreshed (AI can update). + // Version increased = content edited (rename/refactor) → "edited" pulse. + if (m.version !== undefined && old.version !== undefined && m.version > old.version) { + editedNodeIds.push(m.id); + } + return { + ...old, + type: m.type, + name: nameOf(m.properties), + family: familyOf(m.type), + w: nodeDefaultW(m.type), + isReference: m.isReference, + version: m.version, // optimistic concurrency (canvas rename) + implTotal: m.implTotal, implFilled: m.implFilled, implAi: m.implAi, + properties: m.properties, + }; + } + // New node — backend provides position; arrange provides targetX/Y + newNodeIds.push(m.id); + return { + id: m.id, type: m.type, name: nameOf(m.properties), + family: familyOf(m.type), + x: m.position.x, y: m.position.y, + w: nodeDefaultW(m.type), h: NODE_H, + isReference: m.isReference, + version: m.version, + implTotal: m.implTotal, implFilled: m.implFilled, implAi: m.implAi, + properties: m.properties, + enterStart: reducedMotion ? undefined : now, + }; + }); + const index = new Map(nodes.map((n) => [n.id, n])); + + const edges: SceneEdge[] = graph.edges.map((e) => { + const old = prevEdgeIdx?.get(e.id); + if (old) { + return { ...old, kind: e.kind, source: e.sourceNodeId, target: e.targetNodeId }; + } + newEdgeIds.push(e.id); + return { + id: e.id, kind: e.kind, source: e.sourceNodeId, target: e.targetNodeId, + enterStart: reducedMotion ? undefined : now, + }; + }); + return { scene: { nodes, edges, index }, newNodeIds, newEdgeIds, editedNodeIds }; +} + +/** prefers-reduced-motion media query — accessibility. */ +function getReducedMotion(): boolean { + if (typeof window === "undefined" || !window.matchMedia) return false; + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} + +const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(hi, v)); + +export function CanvasView({ graph, onNodeMoved, onContextMenu, onEdgeDrop, onArrange, onApplyLayout, onEdgeDelete }: { + graph: TabGraphData; + onNodeMoved?: (nodeId: string, x: number, y: number) => void; + onContextMenu?: (world: { x: number; y: number }, screen: { x: number; y: number }) => void; + onEdgeDrop?: (nodeId: string, side: "in" | "out", world: { x: number; y: number }, screen: { x: number; y: number }, targetNodeId?: string) => void; + onArrange?: (items: { nodeId: string; x: number; y: number }[]) => void; + /** Programmatic layout apply for Undo/Redo buttons — routed to saveLayout.mutate(items). */ + onApplyLayout?: (items: { nodeId: string; x: number; y: number }[]) => void; + onEdgeDelete?: (edgeId: string) => void; +}) { + const canvasRef = useRef(null); + const vp = useRef({ x: 0, y: 0, zoom: 1 }); + const scene = useRef({ nodes: [], edges: [], index: new Map() }); + const size = useRef({ w: 0, h: 0, dpr: 1 }); + const raf = useRef(0); + const fitted = useRef(false); + const selected = useRef(null); + const hovered = useRef(null); + const pending = useRef<{ sourceId: string; side: "in" | "out"; cur: { x: number; y: number } } | null>(null); + const hud = useRef(null); + const hoveredEdge = useRef(null); + const selectedEdge = useRef(null); + const edgePath = useUiPrefs((s) => s.edgePath); + const edgePathRef = useRef(edgePath); + edgePathRef.current = edgePath; + const edgeBends = useCanvasState((s) => s.edgeBends); + const setBend = useCanvasState((s) => s.setBend); + const edgeBendsRef = useRef(edgeBends); + edgeBendsRef.current = edgeBends; + // Touch mode (coarse pointer) — render loop + hit-tests read it via ref. + const isTouchMode = useTouchMode().isTouch; + const coarseRef = useRef(isTouchMode); + coarseRef.current = isTouchMode; + // Tap-to-connect: "armed" source port — tap port→ARM, next tap connects the target + // (WCAG 2.5.7 drag-free alternative). Render loop reads it from ref for highlight. + const armedPortRef = useRef<{ nodeId: string; side: "in" | "out" } | null>(null); + // Pending AI proposal (green highlight) — render loop reads it via ref. + const proposalNodes = usePendingProposal((s) => s.nodeIds); + const proposalEdges = usePendingProposal((s) => s.edgeIds); + const proposalRef = useRef<{ nodes: Set; edges: Set } | null>(null); + // Global selection store for sidebar Inspector + const selectNode = useSelection((s) => s.selectNode); + const selectedFromStore = useSelection((s) => s.selectedNodeId); + const selectedFromStoreRef = useRef(selectedFromStore); + selectedFromStoreRef.current = selectedFromStore; + // History — disabled state subscription for undo/redo buttons + const canUndo = useHistory((s) => s.past.length > 0); + const canRedo = useHistory((s) => s.future.length > 0); + const onApplyLayoutRef = useRef(onApplyLayout); + onApplyLayoutRef.current = onApplyLayout; + const onEdgeDeleteRef = useRef(onEdgeDelete); + onEdgeDeleteRef.current = onEdgeDelete; + const onEdgeDropRef = useRef(onEdgeDrop); + onEdgeDropRef.current = onEdgeDrop; + + // Optimistic apply: update scene instantly (visual immediate) + write to backend (async) + const applyLayoutItems = (items: { nodeId: string; x: number; y: number }[]) => { + for (const item of items) { + const n = scene.current.index.get(item.nodeId); + if (n) { n.x = item.x; n.y = item.y; } + } + schedule(); + onApplyLayoutRef.current?.(items); + }; + + const onUndoClick = () => { useHistory.getState().undo(); }; + const onRedoClick = () => { useHistory.getState().redo(); }; + + // Soft focus for AI chat NodeChip / EdgeChip — viewport target + highlight halo. + // When vpTarget is set, render loop lerps current viewport towards it each frame, + // becomes undefined when settled. Highlight 600ms fade-out. + const vpTarget = useRef(null); + const focusHighlight = useRef<{ nodeIds: Set; edgeId: string | null; start: number; duration: number } | null>(null); + + // autoBendRatio cache — computed once per edge as long as sceneSig hasn't changed. + // If a node moves during drag, sig changes → entire cache is invalidated, + // recomputed. In steady state, saves O(E) across frames. + const routeCache = useRef<{ sig: number; map: Map }>({ sig: -1, map: new Map() }); + + // Bundle cache — same sceneSig pattern; computed once for all edges (port-spread). + const bundleCache = useRef<{ sig: number; bundles: Bundles | null }>({ sig: -1, bundles: null }); + + // Corridor cache — offsets that spread elbow middle segments side by side (same sig pattern). + const corridorCache = useRef<{ sig: number; map: Map | null }>({ sig: -1, map: null }); + + // Hop cache — crossing hops. O(E²·S²) cost → computed only when the scene is settled + // (no animation/lerp); during animation drawn without hops. + const hopsCache = useRef<{ sig: number; mode: string; map: Map | null }>({ sig: -1, mode: "", map: null }); + + // ── Selection spotlight (focus subgraph) ─────────────────────────── + // focusSet = selected node + its 1-hop neighbours + incident edges. + // Recomputed ONCE per (selection, edge-topology) change — guarded by focusSig, + // NOT per frame. dimAmount lerps 0→1 (focus on) / 1→0 (focus off) for a short fade. + const focusSet = useRef(null); + const focusSig = useRef(""); + const dimAmount = useRef(0); + // Instruct-narration focus — the node currently being highlighted by an instruct + // marker (focusNode({ instruct: true })). This is an ALTERNATE spotlight source, + // independent of canvas selection (it never writes selectedNodeId). When set it + // takes priority over the selection as the spotlight origin; cleared when the + // instruct panel closes / a new stream starts. + const instructFocusId = useRef(null); + + /** Recompute focusSet if the spotlight source (instruct-focus OR selection) or the + * edge topology changed. Cheap O(E) numeric hash per frame; the actual set rebuild + * (also O(E)) only runs when the signature differs (≈ once per select / per instruct + * marker / per AI edge add), not every frame. */ + const ensureFocusSet = () => { + // Spotlight source: active instruct-narration node takes priority, else canvas + // selection. Either one lights up the same selected+1-hop+incident subgraph. + const src = instructFocusId.current ?? selectedFromStoreRef.current; + // Topology hash — folds edge id/source/target into a 32-bit int. Cheap, no + // allocation; changes only when the edge set or its endpoints change. + const sceneEdges = scene.current.edges; + let topoHash = sceneEdges.length; + for (const e of sceneEdges) { + for (let i = 0; i < e.id.length; i++) topoHash = (topoHash * 31 + e.id.charCodeAt(i)) | 0; + for (let i = 0; i < e.source.length; i++) topoHash = (topoHash * 31 + e.source.charCodeAt(i)) | 0; + for (let i = 0; i < e.target.length; i++) topoHash = (topoHash * 31 + e.target.charCodeAt(i)) | 0; + } + const sig = (src ?? "∅") + "#" + topoHash; + if (sig === focusSig.current) return; + focusSig.current = sig; + if (!src || !scene.current.index.has(src)) { + focusSet.current = null; + return; + } + const nodes = new Set([src]); + const edges = new Set(); + for (const e of scene.current.edges) { + if (e.source === src) { edges.add(e.id); nodes.add(e.target); } + else if (e.target === src) { edges.add(e.id); nodes.add(e.source); } + } + focusSet.current = { nodes, edges }; + }; + + // Render-time bend calculation: manual override takes priority, otherwise obstacle-aware auto-route (cached). + const getBendForRender = (edgeId: string): number | undefined => { + const explicit = edgeBendsRef.current[edgeId]; + if (explicit !== undefined) return explicit; + if (edgePathRef.current !== "elbow") return undefined; + const cached = routeCache.current.map.get(edgeId); + if (cached !== undefined) return cached; + const sc = scene.current; + const e = sc.edges.find((x) => x.id === edgeId); + if (!e) return undefined; + const a = sc.index.get(e.source); const b = sc.index.get(e.target); + if (!a || !b) return undefined; + const obstacles = sc.nodes.filter((n) => n.id !== a.id && n.id !== b.id); + const r = autoBendRatio(a, b, obstacles); + routeCache.current.map.set(edgeId, r); + return r; + }; + + const render = () => { + raf.current = 0; + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + // ── AI streaming animation advance ───────────────────────────── + // Nodes/edges whose enterStart duration has elapsed are marked "settled" (undef). + // If targetX/Y exists, current x/y approaches via exponential lerp. + // If at least one node/edge is still animating, frame loop re-arms itself. + const now = performance.now(); + let stillAnimating = false; + for (const n of scene.current.nodes) { + if (n.enterStart !== undefined) { + if (now - n.enterStart >= ANIM_NODE_POP_MS) n.enterStart = undefined; + else stillAnimating = true; + } + if (n.targetX !== undefined && n.targetY !== undefined) { + const dx = n.targetX - n.x; + const dy = n.targetY - n.y; + if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) { + n.x = n.targetX; n.y = n.targetY; + n.targetX = undefined; n.targetY = undefined; + } else { + n.x += dx * ANIM_LERP_FACTOR; + n.y += dy * ANIM_LERP_FACTOR; + stillAnimating = true; + } + } + } + for (const e of scene.current.edges) { + if (e.enterStart !== undefined) { + if (now - e.enterStart >= ANIM_NODE_POP_MS + ANIM_EDGE_DELAY_MS + ANIM_EDGE_FADE_MS) { + e.enterStart = undefined; + } else { + stillAnimating = true; + } + } + } + + // Viewport target lerp — set by focusNode/focusEdge calls + if (vpTarget.current) { + const t = vpTarget.current; + const dx = t.x - vp.current.x; + const dy = t.y - vp.current.y; + const dz = t.zoom - vp.current.zoom; + if (Math.abs(dx) < 0.4 && Math.abs(dy) < 0.4 && Math.abs(dz) < 0.002) { + vp.current = { ...t }; + vpTarget.current = null; + } else { + vp.current.x += dx * ANIM_LERP_FACTOR; + vp.current.y += dy * ANIM_LERP_FACTOR; + vp.current.zoom += dz * ANIM_LERP_FACTOR; + stillAnimating = true; + } + } + // Highlight halo expire + if (focusHighlight.current) { + if (now - focusHighlight.current.start >= focusHighlight.current.duration) { + focusHighlight.current = null; + } else { + stillAnimating = true; + } + } + + // Selection spotlight — recompute focus set once (guarded), lerp dimAmount. + ensureFocusSet(); + const dimTarget = focusSet.current ? 1 : 0; + const dd = dimTarget - dimAmount.current; + if (Math.abs(dd) > 0.002) { + dimAmount.current += dd * FOCUS_FADE_FACTOR; + stillAnimating = true; + } else { + dimAmount.current = dimTarget; + } + + // Defer fit until size is known; fit once when known. + if (!fitted.current && size.current.w > 0 && scene.current.nodes.length > 0) { + fit(); + fitted.current = true; + } + // Cache invalidation — sceneSig is a topology+pos hash; all geometry caches share it. + // Edge set and manual bends also change geometry → included in sig + // (otherwise edge add/remove or bend drag leaves the corridor/hop cache stale). + const ns = scene.current.nodes; + let sig = ns.length; + for (const n of ns) sig = (sig * 31 + n.x * 73 + n.y) | 0; + for (const e of scene.current.edges) { + for (let ci = 0; ci < e.id.length; ci += 7) sig = (sig * 33 + e.id.charCodeAt(ci)) | 0; + } + const bends = edgeBendsRef.current; + for (const k in bends) sig = (sig * 31 + ((bends[k] * 1000) | 0)) | 0; + if (edgePathRef.current === "elbow" && routeCache.current.sig !== sig) { + routeCache.current.sig = sig; + routeCache.current.map.clear(); + } + if (bundleCache.current.sig !== sig) { + bundleCache.current.sig = sig; + bundleCache.current.bundles = computeBundles(scene.current.edges, scene.current.index); + } + const getBundle = (edgeId: string) => { + const b = bundleCache.current.bundles; + if (!b) return undefined; + const s = b.src.get(edgeId) ?? 0; + const t = b.tgt.get(edgeId) ?? 0; + return s === 0 && t === 0 ? undefined : { src: s, tgt: t }; + }; + // Corridor spread — only meaningful in elbow mode (middle segment shift). + if (edgePathRef.current === "elbow") { + if (corridorCache.current.sig !== sig) { + corridorCache.current.sig = sig; + corridorCache.current.map = computeCorridors( + scene.current.edges, scene.current.index, getBendForRender, STUB_LEN_WORLD, portOf, + ); + } + } else { + corridorCache.current.map = null; + corridorCache.current.sig = -1; + } + // Crossing hops — expensive; only when the scene is settled + sufficient zoom. + // During animation the old sig is kept → hopless draw (visually natural: + // a hop on a moving line is unreadable anyway). + if (!stillAnimating && vp.current.zoom >= HOP_MIN_ZOOM) { + if (hopsCache.current.sig !== sig || hopsCache.current.mode !== edgePathRef.current) { + hopsCache.current.sig = sig; + hopsCache.current.mode = edgePathRef.current; + hopsCache.current.map = computeEdgeHops( + scene.current, edgePathRef.current, getBendForRender, + (id) => getBundle(id) ?? { src: 0, tgt: 0 }, + corridorCache.current.map, + ); + } + } else if (stillAnimating) { + hopsCache.current.map = null; + hopsCache.current.sig = -1; + } + ctx.setTransform(size.current.dpr, 0, 0, size.current.dpr, 0, 0); + const p = pending.current; + drawScene(ctx, size.current.w, size.current.h, scene.current, vp.current, selected.current, hovered.current, edgePathRef.current, getBendForRender, hoveredEdge.current, selectedEdge.current, getBundle, now, focusSet.current, dimAmount.current, proposalRef.current, corridorCache.current.map, hopsCache.current.map, coarseRef.current, armedPortRef.current); + + // Focus highlight halo (orange, fade out) — overlay after drawScene + if (focusHighlight.current) { + const fh = focusHighlight.current; + const elapsed = now - fh.start; + const t = Math.min(1, elapsed / fh.duration); + const alpha = Math.max(0, 1 - t); // linear fade out + ctx.save(); + ctx.strokeStyle = `rgba(255, 138, 61, ${alpha * 0.95})`; + ctx.shadowColor = `rgba(255, 138, 61, ${alpha * 0.55})`; + ctx.shadowBlur = 24; + ctx.lineWidth = 3; + for (const nid of fh.nodeIds) { + const n = scene.current.index.get(nid); + if (!n) continue; + const x = n.x * vp.current.zoom + vp.current.x; + const y = n.y * vp.current.zoom + vp.current.y; + const w = n.w * vp.current.zoom; + const h = nodeDisplayH(n) * vp.current.zoom; + const pad = 6; + ctx.beginPath(); + const r = 14; + const rx = x - pad, ry = y - pad, rw = w + pad * 2, rh = h + pad * 2; + ctx.moveTo(rx + r, ry); + ctx.arcTo(rx + rw, ry, rx + rw, ry + rh, r); + ctx.arcTo(rx + rw, ry + rh, rx, ry + rh, r); + ctx.arcTo(rx, ry + rh, rx, ry, r); + ctx.arcTo(rx, ry, rx + rw, ry, r); + ctx.closePath(); + ctx.stroke(); + } + ctx.restore(); + } + + if (stillAnimating) { + // Re-arm frame loop — auto-stops when settled. + raf.current = requestAnimationFrame(render); + return; + } + // Arrow-drag rubber-band (on top of scene) + if (p) { + const s = scene.current.index.get(p.sourceId); + if (s) { + const v = vp.current; + const port = portOf(s, p.side); + const portS = { x: port.x * v.zoom + v.x, y: port.y * v.zoom + v.y }; + const curS = { x: p.cur.x * v.zoom + v.x, y: p.cur.y * v.zoom + v.y }; + // output → cursor (forward); cursor → input (backward) + drawPendingEdge(ctx, p.side === "out" ? portS : curS, p.side === "out" ? curS : portS); + } + } + if (hud.current) hud.current.textContent = armedPortRef.current + ? "Tap the target node to connect · Esc to cancel" + : `${scene.current.nodes.length} node · ${scene.current.edges.length} edge · ${Math.round(vp.current.zoom * 100)}%`; + + // Sync canvas-commands store — BottomBar zoomPercent + NodeActionBar/HoverCard position + useCanvasCommands.getState().set({ + viewport: { ...vp.current }, + nodes: scene.current.nodes, + zoomPercent: vp.current.zoom * 100, + }); + }; + const schedule = () => { if (!raf.current) raf.current = requestAnimationFrame(render); }; + + const resize = () => { + const canvas = canvasRef.current; + if (!canvas) return; + const dpr = window.devicePixelRatio || 1; + const rect = canvas.getBoundingClientRect(); + size.current = { w: rect.width, h: rect.height, dpr }; + canvas.width = Math.round(rect.width * dpr); + canvas.height = Math.round(rect.height * dpr); + schedule(); + }; + + const fit = () => { + const ns = scene.current.nodes; + const { w, h } = size.current; + if (!ns.length || !w) { vp.current = { x: 0, y: 0, zoom: 1 }; return; } + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + for (const n of ns) { + minX = Math.min(minX, n.x); minY = Math.min(minY, n.y); + maxX = Math.max(maxX, n.x + n.w); maxY = Math.max(maxY, n.y + nodeDisplayH(n)); + } + const pad = 80; + const zoom = clamp(Math.min((w - pad * 2) / (maxX - minX || 1), (h - pad * 2) / (maxY - minY || 1)), 0.1, 1.4); + vp.current = { + zoom, + x: w / 2 - ((minX + maxX) / 2) * zoom, + y: h / 2 - ((minY + maxY) / 2) * zoom, + }; + }; + + // Build scene when data changes. Fit only on first load (mount); on refetch + // (e.g. node addition) don't re-fit to avoid viewport jump. + // + // AI streaming: setQueryData triggers this effect for each node/edge. Diff-aware + // buildScene marks new arrivals with enterStart=now → renderer triggers pop animation. + // If new nodes exist, arrange is called and targetX/Y is written → existing nodes + // smooth-slide to new targets, new nodes spawn at target position. Positions are + // persisted to backend via saveLayout → persist across reload. + useEffect(() => { + const prevSel = selected.current; + const prev = scene.current.nodes.length > 0 ? scene.current : null; + const now = performance.now(); + const reducedMotion = getReducedMotion(); + const { scene: newScene, newNodeIds, newEdgeIds, editedNodeIds } = buildScene(graph, prev, now, reducedMotion); + scene.current = newScene; + if (prevSel && !scene.current.index.has(prevSel)) selected.current = null; + + // Edited nodes (AI refactor / rename) → calm in-place "edited" pulse: + // triggers the existing focus halo without panning. schedule() (end of effect) draws it. + if (!reducedMotion && editedNodeIds.length > 0) { + focusHighlight.current = { nodeIds: new Set(editedNodeIds), edgeId: null, start: now, duration: 700 }; + } + + // Arrange trigger: a new node ALWAYS; a new edge only during AI generation + // or right after (covers edges from the post-stream refetch too; + // a manually drawn edge must not disturb the user's hand layout). + const aiEdgeArrived = newEdgeIds.length > 0 && prev !== null && isAiActive(); + if ((newNodeIds.length > 0 || aiEdgeArrived) && scene.current.nodes.length > 1) { + // New node/AI edge arrived → run arrange for entire scene + const pos = arrangeNodes(scene.current.nodes, scene.current.edges, "LR"); + const items: { nodeId: string; x: number; y: number }[] = []; + const newSet = new Set(newNodeIds); + for (const n of scene.current.nodes) { + const p = pos.get(n.id); + if (!p) continue; + if (newSet.has(n.id)) { + // New node: spawn at target position (pop from there) — no layout shift + n.x = p.x; n.y = p.y; + } else if (Math.abs(p.x - n.x) > 0.5 || Math.abs(p.y - n.y) > 0.5) { + // Existing node: smooth lerp from current pos to target + n.targetX = p.x; n.targetY = p.y; + } + items.push({ nodeId: n.id, x: p.x, y: p.y }); + } + // Persist to backend — positions survive page reload + onApplyLayoutRef.current?.(items); + } + schedule(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [graph]); + + // Redraw when edgePath mode changes (graph unchanged, visual only) + useEffect(() => { schedule(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [edgePath]); + + // On theme change (light↔dark) let the renderer re-read CSS variables → redraw once. + useEffect(() => { + const onTheme = () => schedule(); + window.addEventListener("solarch:theme-change", onTheme); + return () => window.removeEventListener("solarch:theme-change", onTheme); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Inline AI proposal — pending green set; on change refresh the ref + redraw. + useEffect(() => { + proposalRef.current = + proposalNodes.size > 0 || proposalEdges.size > 0 + ? { nodes: proposalNodes, edges: proposalEdges } + : null; + schedule(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [proposalNodes, proposalEdges]); + + // Redraw when bend changes (during drag setBend → store → flows here) + useEffect(() => { schedule(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [edgeBends]); + + // Redraw when canonical selection changes (any source: click, AI chip, Inspector) → + // spotlight focus set recomputes + dim transition fades in/out. Also sync the + // local selection-halo ref so the orange highlight follows non-click selection + // sources (AI chip / Inspector), keeping the halo and spotlight on the same node. + useEffect(() => { + selected.current = selectedFromStore; + schedule(); + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [selectedFromStore]); + + // Auto-arrange: run dagre → optimistically update scene → write to backend via parent callback + const doArrange = () => { + const ns = scene.current.nodes; + if (ns.length === 0) return; + // History: "before" snapshot — current position of all nodes + const before = ns.map((n) => ({ nodeId: n.id, x: n.x, y: n.y })); + const pos = arrangeNodes(ns, scene.current.edges, "LR"); + const items: { nodeId: string; x: number; y: number }[] = []; + for (const n of ns) { + const p = pos.get(n.id); + if (!p) continue; + n.x = p.x; n.y = p.y; + items.push({ nodeId: n.id, x: p.x, y: p.y }); + } + fitted.current = false; // re-fit viewport to content after arrange + schedule(); + onArrange?.(items); + if (items.length > 0) { + const beforeSnap = before; + const afterSnap = items; + useHistory.getState().record({ + undo: () => applyLayoutItems(beforeSnap), + redo: () => applyLayoutItems(afterSnap), + }); + } + }; + + // Alt+L shortcut — global (works even without canvas focus) + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === "l") { + e.preventDefault(); + doArrange(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Canvas setup + resize + interaction (imperative, outside React) + useEffect(() => { + const canvas = canvasRef.current!; + resize(); + const ro = new ResizeObserver(resize); + ro.observe(canvas); + + document.fonts.ready.then(() => schedule()); + + const onWheel = (e: WheelEvent) => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + const cx = e.clientX - rect.left, cy = e.clientY - rect.top; + const factor = Math.exp(-e.deltaY * 0.0015); + const z0 = vp.current.zoom; + const z1 = clamp(z0 * factor, 0.1, 4); + // Keep the world point under the cursor fixed + vp.current.x = cx - ((cx - vp.current.x) / z0) * z1; + vp.current.y = cy - ((cy - vp.current.y) / z0) * z1; + vp.current.zoom = z1; + schedule(); + }; + + // screen → world + const toWorld = (clientX: number, clientY: number) => { + const rect = canvas.getBoundingClientRect(); + return { + x: (clientX - rect.left - vp.current.x) / vp.current.zoom, + y: (clientY - rect.top - vp.current.y) / vp.current.zoom, + }; + }; + // Topmost node under cursor (back to front = drawn on top) + const hitTest = (wx: number, wy: number) => { + const ns = scene.current.nodes; + for (let i = ns.length - 1; i >= 0; i--) { + const n = ns[i]; + if (wx >= n.x && wx <= n.x + n.w && wy >= n.y && wy <= n.y + nodeDisplayH(n)) return n; + } + return null; + }; + + // Node port's SCREEN position (in on left / out on right) + const portScreen = (n: SceneNode, side: "in" | "out") => { + const pw = portOf(n, side); + return { x: pw.x * vp.current.zoom + vp.current.x, y: pw.y * vp.current.zoom + vp.current.y }; + }; + // Which port is cursor near? (out / in / null) — for drag initiation + cursor + const nearPort = (n: SceneNode, sxp: number, syp: number): "in" | "out" | null => { + const R = PORT_R + (coarseRef.current ? 28 : 20); // wider port hit-target on touch (WCAG 2.5.8) + const out = portScreen(n, "out"); + if (Math.hypot(sxp - out.x, syp - out.y) <= R) return "out"; + const inP = portScreen(n, "in"); + if (Math.hypot(sxp - inP.x, syp - inP.y) <= R) return "in"; + return null; + }; + + let panning = false; + let dragNode: SceneNode | null = null; + let dragOff = { x: 0, y: 0 }; + let dragStart: { x: number; y: number } | null = null; // "before" position for undo + let moved = false; + let lastX = 0, lastY = 0; + let bendDrag: { edgeId: string; horiz: boolean; corr: number; start: { x: number; y: number }; end: { x: number; y: number } } | null = null; + + // Multi-touch: track each active pointer by its id. Two fingers → pinch-zoom + + // two-finger pan mode. Focus point = midpoint of the two fingers; scaling + // math is identical to onWheel (the world point under the finger stays fixed). + const activePointers = new Map(); + let pinch: { lastDist: number; lastCenter: { x: number; y: number } } | null = null; + + // Touch long-press → context menu (right-click alternative). If the finger is held + // still ~500ms, AddNodeMenu opens; if it slides (pan/drag intent) it is cancelled. + let longPressTimer: ReturnType | null = null; + let pressX = 0, pressY = 0; + const clearLongPress = () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }; + + // Tap-to-connect: screen point where port-drag began — at release a movement threshold + // distinguishes "tap vs drag" (tap → ARM, drag → classic drag-to-connect). + let pendingDown = { x: 0, y: 0 }; + // Scan any node's port under the finger/cursor (not tied to hover). + const scanPort = (sxp: number, syp: number): { node: SceneNode; side: "in" | "out" } | null => { + const ns = scene.current.nodes; + for (let i = ns.length - 1; i >= 0; i--) { + const side = nearPort(ns[i], sxp, syp); + if (side) return { node: ns[i], side }; + } + return null; + }; + + // Elbow handle hit-test: is cursor (in screen coords) near a bend handle? + // Corridor offset applied the same way as drawing (so the handle stays on the wire). + const hitBendHandle = (sxp: number, syp: number) => { + if (edgePathRef.current !== "elbow") return null; + const sc = scene.current; + const v = vp.current; + for (const e of sc.edges) { + const a = sc.index.get(e.source); const b = sc.index.get(e.target); + if (!a || !b) continue; + const g = elbowGeom(a, b, edgeBendsRef.current[e.id] ?? 0.5); + if (!g) continue; + const corr = corridorCache.current.map?.get(e.id) ?? 0; + const hx = (g.handle.x + (g.horiz ? corr : 0)) * v.zoom + v.x; + const hy = (g.handle.y + (g.horiz ? 0 : corr)) * v.zoom + v.y; + if (Math.hypot(sxp - hx, syp - hy) <= BEND_HANDLE_R + (coarseRef.current ? 16 : 4)) { + return { edgeId: e.id, horiz: g.horiz, corr, start: g.start, end: g.end }; + } + } + return null; + }; + + // Point → segment perpendicular distance (screen px) + const pointToSegDist = (px: number, py: number, x1: number, y1: number, x2: number, y2: number) => { + const dx = x2 - x1, dy = y2 - y1; + const len2 = dx * dx + dy * dy; + if (len2 === 0) return Math.hypot(px - x1, py - y1); + let t = ((px - x1) * dx + (py - y1) * dy) / len2; + t = Math.max(0, Math.min(1, t)); + return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy)); + }; + + // Edge hit-test — works in all modes (elbow: middle segment, bezier: sampling, straight: full path) + const hitEdge = (sxp: number, syp: number): string | null => { + const sc = scene.current; + const v = vp.current; + const mode = edgePathRef.current; + const THRESHOLD = coarseRef.current ? 18 : 8; // wider edge hit-target on touch + let best: string | null = null, bestD = Infinity; + for (const e of sc.edges) { + const a = sc.index.get(e.source); const b = sc.index.get(e.target); + if (!a || !b) continue; + + const portOutW = portOf(a, "out"); + const portInW = portOf(b, "in"); + const STUB = STUB_LEN_WORLD * v.zoom; + const portOutS = { x: portOutW.x * v.zoom + v.x, y: portOutW.y * v.zoom + v.y }; + const portInS = { x: portInW.x * v.zoom + v.x, y: portInW.y * v.zoom + v.y }; + const stubOutS = { x: portOutS.x + STUB, y: portOutS.y }; + const stubInS = { x: portInS.x - STUB, y: portInS.y }; + + let d = Infinity; + + if (mode === "elbow") { + const g = elbowGeom(a, b, edgeBendsRef.current[e.id] ?? 0.5); + if (!g) continue; + const corr = corridorCache.current.map?.get(e.id) ?? 0; + const hxc = g.handle.x + (g.horiz ? corr : 0); + const hyc = g.handle.y + (g.horiz ? 0 : corr); + const c1 = g.horiz ? { x: hxc, y: g.start.y } : { x: g.start.x, y: hyc }; + const c2 = g.horiz ? { x: hxc, y: g.end.y } : { x: g.end.x, y: hyc }; + const c1s = { x: c1.x * v.zoom + v.x, y: c1.y * v.zoom + v.y }; + const c2s = { x: c2.x * v.zoom + v.x, y: c2.y * v.zoom + v.y }; + d = pointToSegDist(sxp, syp, c1s.x, c1s.y, c2s.x, c2s.y); + } else if (mode === "straight") { + // Only stubOut→stubIn (port stubs override port hover, not included) + d = pointToSegDist(sxp, syp, stubOutS.x, stubOutS.y, stubInS.x, stubInS.y); + } else { // bezier + const dx = stubInS.x - stubOutS.x; + const dy = stubInS.y - stubOutS.y; + const horiz = Math.abs(dx) >= Math.abs(dy); + const off = Math.min(Math.max(Math.abs(horiz ? dx : dy) * 0.5, 24), 220); + const c1x = horiz ? stubOutS.x + off : stubOutS.x; + const c1y = horiz ? stubOutS.y : stubOutS.y + off; + const c2x = horiz ? stubInS.x - off : stubInS.x; + const c2y = horiz ? stubInS.y : stubInS.y - off; + // Only bezier curve (not stubs — causes noise near ports) + for (let i = 0; i <= 16; i++) { + const t = i / 16, mt = 1 - t; + const bx = mt*mt*mt*stubOutS.x + 3*mt*mt*t*c1x + 3*mt*t*t*c2x + t*t*t*stubInS.x; + const by = mt*mt*mt*stubOutS.y + 3*mt*mt*t*c1y + 3*mt*t*t*c2y + t*t*t*stubInS.y; + d = Math.min(d, Math.hypot(sxp - bx, syp - by)); + } + } + + if (d < THRESHOLD && d < bestD) { bestD = d; best = e.id; } + } + return best; + }; + + const onDown = (e: PointerEvent) => { + const rect = canvas.getBoundingClientRect(); + const sxp = e.clientX - rect.left, syp = e.clientY - rect.top; + const wp = toWorld(e.clientX, e.clientY); + canvas.setPointerCapture(e.pointerId); + moved = false; + + // Multi-touch intent gate: when the second finger lands, switch to pinch mode and + // cancel the single-finger operation the FIRST finger started (node-drag / pan / edge / + // bend) — prevents Excalidraw's spurious-stroke bug. + activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (activePointers.size >= 2) { + clearLongPress(); + if (dragNode && dragStart) { dragNode.x = dragStart.x; dragNode.y = dragStart.y; } // undo nudge + dragNode = null; dragStart = null; pending.current = null; bendDrag = null; panning = false; + armedPortRef.current = null; + useCanvasCommands.getState().set({ isDragging: false }); + const p = [...activePointers.values()]; + pinch = { + lastDist: Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y) || 1, + lastCenter: { x: (p[0].x + p[1].x) / 2, y: (p[0].y + p[1].y) / 2 }, + }; + canvas.style.cursor = "default"; + schedule(); + return; + } + + // Tap-to-connect COMPLETION: while a port is armed, the next tap picks the target + // (port or node body). Empty/same tap = cancel. Drag-free single-pointer path. + if (armedPortRef.current) { + const armed = armedPortRef.current; + armedPortRef.current = null; + const tgt = scanPort(sxp, syp)?.node ?? hitTest(wp.x, wp.y); + if (tgt && tgt.id !== armed.nodeId) { + onEdgeDropRef.current?.(armed.nodeId, armed.side, wp, { x: sxp, y: syp }, tgt.id); + hapticConfirm(); + } + canvas.style.cursor = "default"; + schedule(); + return; + } + + // Touch long-press → context menu (finger held ~500ms ⇒ AddNodeMenu). + // The single-finger logic below (pan/drag) is still set up; if long-press fires it is cancelled. + if (e.pointerType === "touch") { + pressX = e.clientX; pressY = e.clientY; + clearLongPress(); + longPressTimer = setTimeout(() => { + longPressTimer = null; + if (dragNode && dragStart) { dragNode.x = dragStart.x; dragNode.y = dragStart.y; } // undo nudge + dragNode = null; dragStart = null; pending.current = null; panning = false; + useCanvasCommands.getState().set({ isDragging: false }); + try { canvas.releasePointerCapture(e.pointerId); } catch { /* ignore */ } + hapticConfirm(); + const r = canvas.getBoundingClientRect(); + onContextMenu?.(toWorld(pressX, pressY), { x: pressX - r.left, y: pressY - r.top }); + canvas.style.cursor = "default"; + schedule(); + }, 500); + } + + // 0) Elbow bend handle (before node hit-test, before port-drag) + const bh = hitBendHandle(sxp, syp); + if (bh) { + clearLongPress(); // bend handle pressed: drag intent → cancel menu open + bendDrag = bh; + canvas.style.cursor = bh.horiz ? "ew-resize" : "ns-resize"; + return; + } + + // 1) Start port-drag. Mouse: fast path of the hovered port; otherwise (incl. touch) + // scan any port under the finger → drag-to-connect works on touch. + const hov = hovered.current ? scene.current.index.get(hovered.current) : null; + const hovSide = hov ? nearPort(hov, sxp, syp) : null; + const port = hovSide && hov ? { node: hov, side: hovSide } : scanPort(sxp, syp); + if (port) { + clearLongPress(); // port pressed: connect intent → cancel long-press menu + pending.current = { sourceId: port.node.id, side: port.side, cur: wp }; + pendingDown = { x: sxp, y: syp }; // read at release to distinguish tap/drag + hovered.current = port.node.id; + canvas.style.cursor = "crosshair"; + schedule(); + return; + } + + const hit = hitTest(wp.x, wp.y); + if (hit) { + dragNode = hit; + dragOff = { x: wp.x - hit.x, y: wp.y - hit.y }; + dragStart = { x: hit.x, y: hit.y }; // undo "before" snapshot + selected.current = hit.id; + selectedEdge.current = null; // unselect edge when selecting node + // Manual canvas selection wins over an active instruct narration spotlight — + // clears it so selection becomes the sole spotlight source (no stale dim). + instructFocusId.current = null; + selectNode(hit.id); // forward to sidebar Inspector + if (e.pointerType === "touch") hapticTap(); // selection tick (touch) + canvas.style.cursor = "grabbing"; + // ActionBar/HoverCard guard — true only during node drag (not pending edge / bend drag) + useCanvasCommands.getState().set({ isDragging: true }); + } else { + // Empty area: try edge select first + const edgeHit = hitEdge(sxp, syp); + if (edgeHit) { + selectedEdge.current = edgeHit; + selected.current = null; + selectNode(null); // node deselect → inspector closes + canvas.style.cursor = "pointer"; + } else { + panning = true; + selected.current = null; + selectedEdge.current = null; + selectNode(null); + lastX = e.clientX; lastY = e.clientY; + canvas.style.cursor = "grabbing"; + } + } + schedule(); + }; + + const onMove = (e: PointerEvent) => { + // Pinch-zoom + two-finger pan (focus = finger midpoint, math same as onWheel) + if (pinch && activePointers.has(e.pointerId)) { + activePointers.set(e.pointerId, { x: e.clientX, y: e.clientY }); + if (activePointers.size >= 2) { + const p = [...activePointers.values()]; + const dist = Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y) || 1; + const center = { x: (p[0].x + p[1].x) / 2, y: (p[0].y + p[1].y) / 2 }; + const rect = canvas.getBoundingClientRect(); + const cx = center.x - rect.left, cy = center.y - rect.top; + const z0 = vp.current.zoom; + const z1 = clamp(z0 * (dist / pinch.lastDist), 0.1, 4); + // scale around the focus point (point under the finger stays fixed) + vp.current.x = cx - ((cx - vp.current.x) / z0) * z1; + vp.current.y = cy - ((cy - vp.current.y) / z0) * z1; + vp.current.zoom = z1; + // two-finger pan: viewport shifts as the midpoint moves + vp.current.x += center.x - pinch.lastCenter.x; + vp.current.y += center.y - pinch.lastCenter.y; + pinch.lastDist = dist; + pinch.lastCenter = center; + schedule(); + } + return; + } + // Long-press: if the finger slides (pan/drag intent) cancel the pending long-press + if (longPressTimer && Math.hypot(e.clientX - pressX, e.clientY - pressY) > 10) clearLongPress(); + if (bendDrag) { + const wp = toWorld(e.clientX, e.clientY); + const dx = bendDrag.end.x - bendDrag.start.x; + const dy = bendDrag.end.y - bendDrag.start.y; + // Corridor offset is added in drawing, so it's subtracted in drag — the handle tracks the cursor. + const ratio = bendDrag.horiz + ? (wp.x - bendDrag.corr - bendDrag.start.x) / (dx || 1) + : (wp.y - bendDrag.corr - bendDrag.start.y) / (dy || 1); + setBend(bendDrag.edgeId, ratio); // store does clamp01 + return; // re-render triggered via useEffect[edgeBends] + } + if (pending.current) { + pending.current.cur = toWorld(e.clientX, e.clientY); + schedule(); + } else if (dragNode) { + const wp = toWorld(e.clientX, e.clientY); + // 8px snap-to-grid — nodes align neatly; free when Alt is held. + const SNAP = 8; + const free = e.altKey; + const nx = wp.x - dragOff.x, ny = wp.y - dragOff.y; + dragNode.x = free ? Math.round(nx) : Math.round(nx / SNAP) * SNAP; + dragNode.y = free ? Math.round(ny) : Math.round(ny / SNAP) * SNAP; + moved = true; + schedule(); + } else if (panning) { + vp.current.x += e.clientX - lastX; + vp.current.y += e.clientY - lastY; + lastX = e.clientX; lastY = e.clientY; + schedule(); + } else { + // hover: cursor + port + edge handle/path + const rect = canvas.getBoundingClientRect(); + const sxp = e.clientX - rect.left, syp = e.clientY - rect.top; + const wp = toWorld(e.clientX, e.clientY); + let hit = hitTest(wp.x, wp.y); + if (!hit && hovered.current) { + const cur = scene.current.index.get(hovered.current); + if (cur && nearPort(cur, sxp, syp) !== null) hit = cur; + } + // Edge hit only when no node (node takes priority) + const edgeHit = !hit ? hitEdge(sxp, syp) : null; + const bhHover = !hit && edgeHit ? hitBendHandle(sxp, syp) : null; + + const prevHover = hovered.current; + const prevEdgeHover = hoveredEdge.current; + hovered.current = hit ? hit.id : null; + hoveredEdge.current = edgeHit; + + const onP = hit ? nearPort(hit, sxp, syp) !== null : false; + canvas.style.cursor = + bhHover ? (bhHover.horiz ? "ew-resize" : "ns-resize") : + onP ? "crosshair" : + hit ? "grab" : + edgeHit ? "pointer" : "default"; + + if (hovered.current !== prevHover) { + useSelection.getState().setHovered(hovered.current); + } + if (hovered.current !== prevHover || hoveredEdge.current !== prevEdgeHover) schedule(); + } + }; + + const onUp = (e: PointerEvent) => { + try { canvas.releasePointerCapture(e.pointerId); } catch { /* ignore */ } + activePointers.delete(e.pointerId); + clearLongPress(); + // Skip drag-commit logic during pinch; end pinch when fewer than two fingers remain. + if (pinch) { + if (activePointers.size < 2) { pinch = null; panning = false; canvas.style.cursor = "default"; } + return; + } + if (bendDrag) { + bendDrag = null; + canvas.style.cursor = "default"; + return; + } + const rect = canvas.getBoundingClientRect(); + if (pending.current) { + const upX = e.clientX - rect.left, upY = e.clientY - rect.top; + const wp = toWorld(e.clientX, e.clientY); + const { sourceId, side } = pending.current; + pending.current = null; + hovered.current = null; + // TAP (motionless release) → ARM the source (tap-to-connect; drag-free, WCAG 2.5.7). + if (Math.hypot(upX - pendingDown.x, upY - pendingDown.y) < 8) { + armedPortRef.current = { nodeId: sourceId, side }; + hapticTap(); + canvas.style.cursor = "default"; + schedule(); + return; + } + // DRAG → classic drag-to-connect. Release over the source = cancel. + const dropTarget = hitTest(wp.x, wp.y); + if (dropTarget && dropTarget.id === sourceId) { + canvas.style.cursor = "default"; + schedule(); + return; + } + onEdgeDropRef.current?.(sourceId, side, wp, { x: upX, y: upY }, dropTarget ? dropTarget.id : undefined); + canvas.style.cursor = "default"; + schedule(); + return; + } + if (dragNode && moved && onNodeMoved) { + onNodeMoved(dragNode.id, dragNode.x, dragNode.y); // save new position + // History: single node move — DON'T record if snap results in equal positions + if (dragStart) { + const changed = dragStart.x !== dragNode.x || dragStart.y !== dragNode.y; + if (changed) { + const beforeSnap = [{ nodeId: dragNode.id, x: dragStart.x, y: dragStart.y }]; + const afterSnap = [{ nodeId: dragNode.id, x: dragNode.x, y: dragNode.y }]; + useHistory.getState().record({ + undo: () => applyLayoutItems(beforeSnap), + redo: () => applyLayoutItems(afterSnap), + }); + } + } + } + if (dragNode) { + // ActionBar/HoverCard visible again after drag ends + useCanvasCommands.getState().set({ isDragging: false }); + } + dragNode = null; + dragStart = null; + panning = false; + canvas.style.cursor = "default"; + }; + + const onCtx = (e: MouseEvent) => { + e.preventDefault(); + const rect = canvas.getBoundingClientRect(); + onContextMenu?.(toWorld(e.clientX, e.clientY), { x: e.clientX - rect.left, y: e.clientY - rect.top }); + }; + + // Double-click → select node + open EditorModal + auto-focus Inspector's first input. + // openEditor is atomic: editorNodeId + selectedNodeId + editingNodeId set together. + const onDblClick = (e: MouseEvent) => { + const wp = toWorld(e.clientX, e.clientY); + const hit = hitTest(wp.x, wp.y); + if (hit) { + e.preventDefault(); + selected.current = hit.id; + useSelection.getState().openEditor(hit.id); + schedule(); + } + }; + + // ⌘Z/⌘⇧Z + Delete global hotkeys (F2 rename in AppShell) + const onKeyDown = (e: KeyboardEvent) => { + const t = e.target as HTMLElement; + const inForm = t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable; + + // Escape → cancel the armed tap-to-connect source + if (e.key === "Escape" && armedPortRef.current) { + armedPortRef.current = null; + schedule(); + return; + } + + // Delete / Backspace → delete selected edge + if ((e.key === "Delete" || e.key === "Backspace") && !inForm && selectedEdge.current) { + e.preventDefault(); + const eid = selectedEdge.current; + selectedEdge.current = null; + schedule(); + onEdgeDeleteRef.current?.(eid); + return; + } + + // ⌘Z / Ctrl+Z → undo, ⌘+Shift+Z → redo + const mod = e.metaKey || e.ctrlKey; + if (mod && e.key.toLowerCase() === "z" && !inForm) { + e.preventDefault(); + if (e.shiftKey) { + useHistory.getState().redo(); + } else { + useHistory.getState().undo(); + } + } + }; + + canvas.addEventListener("wheel", onWheel, { passive: false }); + canvas.addEventListener("pointerdown", onDown); + canvas.addEventListener("pointermove", onMove); + canvas.addEventListener("pointerup", onUp); + canvas.addEventListener("pointercancel", onUp); // pointer cancel → prevent state leak + canvas.addEventListener("contextmenu", onCtx); + canvas.addEventListener("dblclick", onDblClick); + window.addEventListener("keydown", onKeyDown); + + return () => { + ro.disconnect(); + canvas.removeEventListener("wheel", onWheel); + canvas.removeEventListener("pointerdown", onDown); + canvas.removeEventListener("pointermove", onMove); + canvas.removeEventListener("pointerup", onUp); + canvas.removeEventListener("pointercancel", onUp); + canvas.removeEventListener("contextmenu", onCtx); + canvas.removeEventListener("dblclick", onDblClick); + window.removeEventListener("keydown", onKeyDown); + if (raf.current) { cancelAnimationFrame(raf.current); raf.current = 0; } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Soft pan+zoom to a node + 600ms orange highlight. For AI chat NodeChip. + // + // Two contexts: + // - selection / chip click (default): keeps the existing zoom-in behaviour and + // centers the node in the viewport. Does NOT touch the spotlight source. + // - instruct narration (opts.instruct): the node is the active narration subject. + // (a) zoom a touch LOWER (zoom-out) so the node + its neighbours fit, (b) shift + // the node UP into the clean area above the bottom explanation panel (reserve the + // bottom `reserveBottom` fraction of the viewport), and (c) make this node the + // spotlight source so it + 1-hop neighbours stay lit while the rest dims. + const doFocusNode = (id: string, opts?: { zoom?: boolean; instruct?: boolean; reserveBottom?: number }) => { + const n = scene.current.index.get(id); + if (!n) return; + const cx = n.x + n.w / 2; + const cy = n.y + nodeDisplayH(n) / 2; + + if (opts?.instruct) { + // Instruct spotlight source — recomputed in ensureFocusSet (sig folds this id). + instructFocusId.current = id; + // Zoom-out vs. the selection focus: cap lower + scale down so the focused node + // plus its 1-hop neighbourhood comfortably fit on screen (no edge-to-edge crop). + const targetZoom = clamp(Math.min(vp.current.zoom, 1.0) * 0.85, 0.45, 1.1); + // Reserve the bottom `reserveBottom` fraction for the explanation panel; center + // the node vertically in the remaining clean band (top → reserve line). + const reserve = clamp(opts.reserveBottom ?? 0.42, 0, 0.7); + const cleanH = size.current.h * (1 - reserve); + vpTarget.current = { + zoom: targetZoom, + x: size.current.w / 2 - cx * targetZoom, + y: cleanH / 2 - cy * targetZoom, + }; + } else { + const targetZoom = opts?.zoom + ? clamp(Math.max(vp.current.zoom, 1.0) * 1.25, 0.5, 1.8) + : vp.current.zoom; + vpTarget.current = { + zoom: targetZoom, + x: size.current.w / 2 - cx * targetZoom, + y: size.current.h / 2 - cy * targetZoom, + }; + } + focusHighlight.current = { nodeIds: new Set([id]), edgeId: null, start: performance.now(), duration: 600 }; + schedule(); + }; + + // Clear instruct-narration spotlight source. The selection spotlight (if any) + // resumes; otherwise dim fades back to 0. ensureFocusSet picks this up via its sig. + const doClearInstructFocus = () => { + if (instructFocusId.current === null) return; + instructFocusId.current = null; + schedule(); + }; + + const doFocusEdge = (id: string) => { + const e = scene.current.edges.find((x) => x.id === id); + if (!e) return; + const a = scene.current.index.get(e.source); + const b = scene.current.index.get(e.target); + if (!a || !b) return; + // Fit bounds so both edge endpoints are visible (don't zoom — only pan) + const ax = a.x + a.w / 2, ay = a.y + nodeDisplayH(a) / 2; + const bx = b.x + b.w / 2, by = b.y + nodeDisplayH(b) / 2; + const cx = (ax + bx) / 2, cy = (ay + by) / 2; + vpTarget.current = { + zoom: vp.current.zoom, + x: size.current.w / 2 - cx * vp.current.zoom, + y: size.current.h / 2 - cy * vp.current.zoom, + }; + focusHighlight.current = { + nodeIds: new Set([e.source, e.target]), + edgeId: id, + start: performance.now(), + duration: 600, + }; + schedule(); + }; + + // Register callbacks to BottomBar canvas-commands store — mount/unmount cycle + useEffect(() => { + useCanvasCommands.getState().set({ + fit: () => { fit(); schedule(); }, + zoomIn: () => { vp.current.zoom = clamp(vp.current.zoom * 1.2, 0.1, 4); schedule(); }, + zoomOut: () => { vp.current.zoom = clamp(vp.current.zoom / 1.2, 0.1, 4); schedule(); }, + arrange: doArrange, + undo: onUndoClick, + redo: onRedoClick, + focusNode: doFocusNode, + focusEdge: doFocusEdge, + clearInstructFocus: doClearInstructFocus, + }); + return () => { + useCanvasCommands.getState().set({ + fit: null, zoomIn: null, zoomOut: null, arrange: null, undo: null, redo: null, + focusNode: null, focusEdge: null, clearInstructFocus: null, + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Keep store up-to-date when canUndo/canRedo changes + useEffect(() => { + useCanvasCommands.getState().set({ canUndo, canRedo }); + }, [canUndo, canRedo]); + + return ( +
+ {/* Canvas is decorative (aria-hidden) — semantics come from the invisible DOM mirror below. */} + + + {/* ── Right content ── */} +
+
+
+ {section === "nodes" ? "Node Library" : + section === "edges" ? "Edge Library" : "Keyboard Shortcuts"} +
+ +
+ +
+ {section === "nodes" && } + {section === "edges" && } + {section === "shortcuts" && } +
+
+ + + + ); +} + +function SectionTab({ active, onClick, icon, label, count }: { + active: boolean; onClick: () => void; icon: React.ReactNode; label: string; count: number; +}) { + return ( + + ); +} + +/** Node type item — family-tinted icon box + name + active accent strip. */ +function NodeItemBtn({ active, onClick, accent, type }: { + active: boolean; onClick: () => void; accent: string; type: string; +}) { + return ( + + ); +} + +/** Edge kind item — mono badge + active strip. */ +function EdgeItemBtn({ active, onClick, kind }: { + active: boolean; onClick: () => void; kind: string; +}) { + return ( + + ); +} + +function EmptyState({ query }: { query: string }) { + return ( +
+
+ // No results for "{query}" +
+
+ ); +} + +function NodeDetail({ type }: { type: string }) { + const doc = NODE_DOCS.find((n) => n.type === type); + if (!doc) return
No node selected.
; + const accent = colorOf(doc.type); + + return ( +
+ {/* Hero */} +
+
+
+ +
+
+
+ {doc.familyLabel} · {familyOf(doc.type)} +
+

+ {doc.type} +

+
+
+
+ +
+

{doc.summary}

+
+ +
+
    + {doc.whereUsed.map((w, i) => ( +
  • + + {w} +
  • + ))} +
+
+ +
+
+ {doc.examples.map((ex, i) => ( + + {ex} + + ))} +
+
+ +
+
    + {doc.commonEdges.map((e, i) => ( +
  • + {e} +
  • + ))} +
+
+
+ ); +} + +function EdgeDetail({ kind }: { kind: string }) { + const doc = EDGE_DOCS.find((e) => e.kind === kind); + if (!doc) return
No edge selected.
; + + return ( +
+
+
+ {doc.category} +
+

+ {doc.kind} +

+
+ +
+

{doc.summary}

+
+ +
+
    + {doc.whenToUse.map((w, i) => ( +
  • + + {w} +
  • + ))} +
+
+ +
+
+ {doc.examples.map((ex, i) => ( + + {ex} + + ))} +
+
+
+ ); +} + +function ShortcutsList() { + const groups = useMemo(() => { + const map = new Map(); + for (const s of SHORTCUT_DOCS) { + const arr = map.get(s.group) ?? []; + arr.push(s); + map.set(s.group, arr); + } + return [...map.entries()]; + }, []); + + return ( +
+ {groups.map(([grp, items]) => ( +
+

+ {grp} +

+
+ {items.map((s, i) => ( +
+ + {s.keys} + + + {s.description} + +
+ ))} +
+
+ ))} +
+ ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

+ {title} +

+ {children} +
+ ); +} diff --git a/apps/web/src/components/EditorModal.tsx b/apps/web/src/components/EditorModal.tsx new file mode 100644 index 0000000..8131b7c --- /dev/null +++ b/apps/web/src/components/EditorModal.tsx @@ -0,0 +1,71 @@ +/** EditorModal — Linear/Notion style premium center modal + inline subpage navigation. + * Replaces Vaul bottom drawers: subpage fills the modal (← Back + Save). + * InspectorPanel is the default page; Service/Controller/Table/DTO drawers push subpages. */ + +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { Z_LAYERS } from "../lib/z-layers"; +import { useSelection } from "../state/selection"; +import { InspectorPanel } from "./Inspector/InspectorPanel"; +import { cn } from "@/lib/utils"; + +/** Backwards-compatible no-op — avoids breaking imports from old callers. + * New pattern: each inspector holds its own sub-page state (stale snapshot bug fix). */ +export function useEditorSubPage() { + return { current: null, open: () => {}, close: () => {} }; +} + +export function EditorModal() { + const editorNodeId = useSelection((s) => s.editorNodeId); + const closeEditor = useSelection((s) => s.closeEditor); + + // The modal's single source is editorNodeId. A single click triggers selectNode but doesn't + // touch editorNodeId → modal won't open with stale state. closeEditor closes atomically. + const isOpen = !!editorNodeId; + const setOpen = (next: boolean) => { if (!next) closeEditor(); }; + + return ( + + + + + { + e.preventDefault(); + closeEditor(); + }} + onOpenAutoFocus={(e) => { e.preventDefault(); }} + className={cn( + "fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2", + "w-[min(1100px,94vw)] h-[88vh] max-h-[88vh]", + "flex flex-col overflow-hidden", + "rounded-xl border border-border", + "bg-[color:var(--paper-raised)]", + "shadow-[0_24px_80px_-20px_rgba(11,16,32,0.40)]", + "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", + "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", + "data-[state=open]:duration-200 data-[state=closed]:duration-150", + "focus:outline-none" + )} + style={{ zIndex: Z_LAYERS.MODAL + 1 }} + > + Node editor + + Edit the fields of the selected node + + + {/* InspectorPanel handles sub-page navigation internally (state-based) */} + + + + + ); +} diff --git a/apps/web/src/components/Inspector/GenericForm.tsx b/apps/web/src/components/Inspector/GenericForm.tsx new file mode 100644 index 0000000..6ddbe13 --- /dev/null +++ b/apps/web/src/components/Inspector/GenericForm.tsx @@ -0,0 +1,30 @@ +import type { FieldHint } from "../../api/node-types"; +import type { JSONSchema } from "./schema-utils"; +import { SchemaForm } from "./SchemaForm"; + +/** GenericForm — renders an inspector form from a backend node-type JSON Schema. + * All logic is in SchemaForm + widgets/. This wrapper only checks "is schema loaded?". */ +export function GenericForm({ + projectId, nodeId, properties, fieldHints, schema, +}: { + projectId: string; + nodeId: string; + tabId: string; + type: string; + properties: Record; + fieldHints: Record; + schema?: JSONSchema; +}) { + if (!schema) { + return
loading schema…
; + } + return ( + + ); +} diff --git a/apps/web/src/components/Inspector/Inspector.css b/apps/web/src/components/Inspector/Inspector.css new file mode 100644 index 0000000..fd71738 --- /dev/null +++ b/apps/web/src/components/Inspector/Inspector.css @@ -0,0 +1,125 @@ +/* ════════════════════════════════════════════════════════════════════════ + Inspector — p-* classes used (post Phase 6). + All primitives now work with shadcn/ui + Tailwind utilities. + These classes are still consumed by consumers (custom inspector + drawer + schema form); + @apply for Tailwind shorthand. + ──────────────────────────────────────────────────────────────────── */ + +/* ── Form layout (SchemaForm + custom inspector) — modal-optimized ── */ +.p-form { + @apply flex flex-col gap-5; +} +.p-fields { + @apply flex flex-col gap-[18px]; +} +.p-group { + @apply flex flex-col gap-3; +} +.p-group-body { + @apply flex flex-col gap-[18px]; +} + +/* ── Grid layouts (ColumnRow, EndpointCard, FieldCard inside drawers) */ +.p-grid-2 { + @apply grid grid-cols-2 gap-3.5; +} +.p-grid-3 { + @apply grid grid-cols-[1fr_1fr_auto] gap-3.5 items-end; +} + +/* ── Chip grid (Enum widget, DTO flags) ──────────────────────────── */ +.p-chips { + @apply flex flex-wrap gap-1; +} + +/* ── Typography utilities ────────────────────────────────────────── */ +.p-mono { + @apply font-mono text-[11.5px] tracking-[-0.01em]; +} +.p-helper { + @apply text-[10.5px] text-[color:var(--ink-faint)] leading-[1.35] mt-[2px] tracking-[0.01em]; +} +.p-helper-tight { + @apply mt-0 mb-[6px]; +} +.p-badge { + @apply font-mono text-[9px] px-[5px] py-px rounded-[3px] bg-[color:var(--accent-wash)] text-[color:var(--accent-ink)] tracking-[0.04em]; +} +.p-req { + @apply text-[color:var(--danger)] font-semibold text-[11px] -ml-0.5; +} + +/* ── Input size variants (column length input, status code input) ── */ +.p-input-tiny { + @apply !w-14 shrink-0 !px-[6px] !py-1 text-center; +} +.p-input-sm { + @apply !w-[100px] shrink-0; +} + +/* ── Service/DTO method signature row (summary inside ListRow) ───── */ +.p-method-sig { + @apply inline-flex items-center gap-[6px] min-w-0 overflow-hidden; +} +.p-method-name { + @apply text-[color:var(--ink)] font-semibold text-[11.5px] font-mono whitespace-nowrap overflow-hidden text-ellipsis; +} +.p-method-arrow { + @apply text-[color:var(--ink-faint)] text-[11px] shrink-0; +} +.p-method-return { + @apply text-[color:var(--ink-soft)] text-[11.5px] font-mono whitespace-nowrap overflow-hidden text-ellipsis; +} + +/* ── Controller endpoint meta + base route slot ──────────────────── */ +.p-route-meta { + @apply text-[color:var(--ink-faint)] text-[10.5px]; +} +.p-baseroute-row { + @apply inline-flex items-center gap-[6px]; +} +.p-baseroute-label { + @apply font-mono text-[9.5px] uppercase tracking-[0.08em] text-[color:var(--ink-faint)] font-semibold; +} +.p-baseroute-val { + @apply text-[color:var(--ink)] font-semibold; +} + +/* ── Controller rate-limit collapsible ───────────────────────────── */ +.p-rate { + @apply border border-dashed border-[color:var(--hairline-strong)] rounded-[5px] bg-[var(--ins-track)]; +} +.p-rate-summary { + @apply list-none cursor-pointer px-[10px] py-[6px] font-mono text-[10.5px] text-[color:var(--ink-soft)] select-none; +} +.p-rate-summary::-webkit-details-marker { + display: none; +} +.p-rate[open] > .p-rate-summary { + @apply text-[color:var(--ink)] border-b border-dashed border-[color:var(--hairline)]; +} +.p-rate-body { + @apply px-3 pt-[10px] pb-3; +} + +/* ── DTO validation rule empty value placeholder ─────────────────── */ +.p-rule-empty { + @apply text-[color:var(--ink-faint)] font-mono text-[11.5px] px-2; +} + +/* ── Table column state icon — family-colored dot ────────────────── */ +.p-state-dot { + @apply inline-block w-[6px] h-[6px] rounded-full; + background: var(--ins-family-accent); +} + +/* ── Object widget header (legacy span) ──────────────────────────── */ +.p-obj-head { + @apply inline-flex items-center gap-[6px]; +} + +/* ── Global utility (also called outside Inspector) ───────────────── */ +.mono { + font-family: var(--font-mono); + letter-spacing: -0.01em; +} diff --git a/apps/web/src/components/Inspector/InspectorPanel.tsx b/apps/web/src/components/Inspector/InspectorPanel.tsx new file mode 100644 index 0000000..3c83085 --- /dev/null +++ b/apps/web/src/components/Inspector/InspectorPanel.tsx @@ -0,0 +1,119 @@ +import { useEffect, useRef } from "react"; +import { useParams } from "react-router-dom"; +import { Code2 } from "lucide-react"; +import { useSelection } from "../../state/selection"; +import { useNode } from "../../api/nodes"; +import { useNodeType } from "../../api/node-types"; +import { colorOf, familyOf, nameOf } from "../../canvas/families"; +import { GenericForm } from "./GenericForm"; +import { TableInspector } from "./types/TableInspector"; +import { ServiceInspector } from "./types/ServiceInspector"; +import { ControllerInspector } from "./types/ControllerInspector"; +import { DTOInspector } from "./types/DTOInspector"; +import type { JSONSchema } from "./schema-utils"; +import { InspectorShell } from "./primitives"; +import "./Inspector.css"; + +/** Contextual node inspector that sits inside the left slide-in LeftDrawer. + * Delete action is via NodeActionBar / Del shortcut — no delete UI in this panel (Linear/Figma pattern). */ +export function InspectorPanel() { + const { projectId = "" } = useParams<{ projectId: string }>(); + const editorNodeId = useSelection((s) => s.editorNodeId); + const closeEditor = useSelection((s) => s.closeEditor); + const editingNodeId = useSelection((s) => s.editingNodeId); + const stopEditing = useSelection((s) => s.stopEditing); + const { data: node, isLoading } = useNode(projectId, editorNodeId); + const { data: nodeType } = useNodeType(node?.type ?? null); + const bodyRef = useRef(null); + + // Double-click / F2 → editingNodeId → first input auto-focus + select. + useEffect(() => { + if (!node || editingNodeId !== node.id) return; + const handle = requestAnimationFrame(() => { + const root = bodyRef.current; + if (!root) return; + const inp = root.querySelector( + "input:not([type='number']):not([type='checkbox']):not([readonly]):not([disabled]), textarea:not([readonly]):not([disabled])" + ); + if (inp) { + inp.focus(); + try { (inp as HTMLInputElement).select(); } catch { /* skip if textarea can't select */ } + } + stopEditing(); + }); + return () => cancelAnimationFrame(handle); + }, [node, editingNodeId, stopEditing]); + + if (!editorNodeId) return null; + + if (isLoading || !node) { + return ( +
+
+
loading…
+
+ ); + } + + const shared = { + projectId, + nodeId: node.id, + tabId: node.homeTabId, + properties: node.properties, + }; + + const family = familyOf(node.type); + const familyColor = colorOf(node.type); + + const renderBody = () => { + switch (node.type) { + case "Table": return ; + case "Service": return ; + case "Controller": return ; + case "DTO": return ; + default: + return ( + + ); + } + }; + + // "Show Code" — TopBar listener gates + opens panel; detail.focusNodeId focuses + // on this node's generated file. If not entitled, TopBar redirects to /billing. + const onShowCode = () => { + window.dispatchEvent( + new CustomEvent("solarch:codegen-open", { detail: { focusNodeId: node.id } }), + ); + }; + + return ( + closeEditor()} + bodyRef={bodyRef} + headerActions={ + + } + > + {renderBody()} + + ); +} diff --git a/apps/web/src/components/Inspector/SchemaFields.tsx b/apps/web/src/components/Inspector/SchemaFields.tsx new file mode 100644 index 0000000..8e306ce --- /dev/null +++ b/apps/web/src/components/Inspector/SchemaFields.tsx @@ -0,0 +1,143 @@ +/** SchemaFields — renders the properties of an object schema. + * Recursive: also called from nested object/array widgets. */ + +import { useMemo } from "react"; +import type { JSONSchema } from "./schema-utils"; +import { deref, getWidgetKind, humanize, isRequired, isSystemField, lookupHint } from "./schema-utils"; +import type { FieldHint } from "../../api/node-types"; +import { SectionHeader } from "./primitives"; +import { StringWidget } from "./widgets/StringWidget"; +import { NumberWidget } from "./widgets/NumberWidget"; +import { BooleanWidget } from "./widgets/BooleanWidget"; +import { EnumWidget } from "./widgets/EnumWidget"; +import { ArrayWidget } from "./widgets/ArrayWidget"; +import { ObjectWidget } from "./widgets/ObjectWidget"; + +interface SchemaFieldsProps { + rootSchema: JSONSchema; + schema: JSONSchema; + value: Record; + onChange: (next: Record) => void; + fieldHints: Record; + pathPrefix: string; + /** ID of the node being edited — for NodeRefCombobox linkAs (edge creation source) */ + sourceNodeId?: string; +} + +export function SchemaFields({ + rootSchema, schema, value, onChange, fieldHints, pathPrefix, sourceNodeId, +}: SchemaFieldsProps) { + const objSchema = deref(rootSchema, schema); + + const grouped = useMemo(() => { + if (!objSchema.properties) return [] as Array<[string, string[]]>; + const groups = new Map(); + const isRoot = pathPrefix === ""; + for (const key of Object.keys(objSchema.properties)) { + if (isRoot && isSystemField(key)) continue; + const path = pathPrefix ? `${pathPrefix}.${key}` : key; + const g = lookupHint(fieldHints, path, key)?.group ?? ""; + if (!groups.has(g)) groups.set(g, []); + groups.get(g)!.push(key); + } + return [...groups.entries()]; + }, [objSchema, fieldHints, pathPrefix]); + + if (!objSchema.properties) return null; + + return ( +
+ {grouped.map(([group, keys], gIdx) => ( +
+ {group && 0} />} +
+ {keys.map((key) => { + const propSchema = objSchema.properties![key]; + const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key; + const required = isRequired(objSchema, key); + return ( + onChange({ ...value, [key]: v })} + fieldHints={fieldHints} + pathPrefix={fullPath} + required={required} + sourceNodeId={sourceNodeId} + /> + ); + })} +
+
+ ))} +
+ ); +} + +interface FieldDispatchProps { + rootSchema: JSONSchema; + fieldKey: string; + propSchema: JSONSchema; + value: unknown; + onChange: (v: unknown) => void; + fieldHints: Record; + pathPrefix: string; + required?: boolean; + /** If empty string is given, label is not rendered (for array primitive items). */ + labelOverride?: string; + /** Root node ID being edited — for linkAs in nodeRef-hinted fields. */ + sourceNodeId?: string; +} + +export function FieldDispatch({ + rootSchema, fieldKey, propSchema, value, onChange, fieldHints, pathPrefix, required, labelOverride, sourceNodeId, +}: FieldDispatchProps) { + const resolved = deref(rootSchema, propSchema); + const kind = getWidgetKind(resolved); + const label = labelOverride !== undefined ? labelOverride : humanize(fieldKey); + const hint = lookupHint(fieldHints, pathPrefix, fieldKey); + const common = { + label, + fieldKey, + value, + onChange, + hint, + required, + description: resolved.description, + }; + + switch (kind) { + case "enum": + return ; + case "string": + return ; + case "number": + return ; + case "boolean": + return ; + case "array": + return ( + + ); + case "object": + return ( + + ); + } +} diff --git a/apps/web/src/components/Inspector/SchemaForm.tsx b/apps/web/src/components/Inspector/SchemaForm.tsx new file mode 100644 index 0000000..16c4d10 --- /dev/null +++ b/apps/web/src/components/Inspector/SchemaForm.tsx @@ -0,0 +1,164 @@ +/** SchemaForm — renders an inspector form from a backend node-type JSON Schema. + * Controller pattern: scalar fields (string/number/bool/enum) directly in the top form. + * Array / nested object fields as DrawerTrigger buttons in a "Behavior" section → + * push to in-modal subpage. Consistent UX for all 21 types. */ + +import { useMemo, useState } from "react"; +import { useInspectorUpdate } from "../../hooks/useInspectorUpdate"; +import { deref, getWidgetKind, humanize, isSystemField, isRequired, type JSONSchema } from "./schema-utils"; +import type { FieldHint } from "../../api/node-types"; +import { SaveStatus, SectionHeader, DrawerTrigger, SubPageShell } from "./primitives"; +import { FieldDispatch } from "./SchemaFields"; + +interface SchemaFormProps { + projectId: string; + nodeId: string; + properties: Record; + fieldHints: Record; + schema: JSONSchema; +} + +/** Which keys are scalar (inline form) / which are complex (subpage trigger). */ +function splitKeys(schema: JSONSchema, rootSchema: JSONSchema) { + const scalar: string[] = []; + const complex: string[] = []; + if (!schema.properties) return { scalar, complex }; + for (const key of Object.keys(schema.properties)) { + if (isSystemField(key)) continue; + const propSchema = deref(rootSchema, schema.properties[key]); + const kind = getWidgetKind(propSchema); + if (kind === "array" || kind === "object") complex.push(key); + else scalar.push(key); + } + return { scalar, complex }; +} + +export function SchemaForm({ + projectId, nodeId, properties, fieldHints, schema, +}: SchemaFormProps) { + const { draft, setAll, update } = useInspectorUpdate(projectId, nodeId, properties); + // Inline subpage state — no modal context (stale snapshot bug fix) + const [activeKey, setActiveKey] = useState(null); + + const status: "idle" | "pending" | "success" | "error" = + update.isPending ? "pending" : + update.isError ? "error" : + update.isSuccess ? "success" : + "idle"; + const errorMessage = update.error instanceof Error ? update.error.message : undefined; + + // Backend sometimes returns a "Node wrapper schema" ({id, type, ..., properties: {...}}). + // Form draft = node.properties, i.e. the contents of the "properties" sub-object. If the schema + // has a nested "properties" key, unwrap it — otherwise form fields won't match the schema + // (a single DrawerTrigger "Properties" would appear containing Name/Description). + const formSchema = useMemo(() => { + if (schema.properties?.properties) { + const inner = deref(schema, schema.properties.properties); + if (inner.properties) return inner; + } + return schema; + }, [schema]); + + const { scalar: scalarKeys, complex: complexKeys } = useMemo( + () => splitKeys(formSchema, formSchema), + [formSchema], + ); + + // Find the Name field for subpage subtitle (possible keys for each type) + const nameField = useMemo(() => { + for (const k of ["Name", "TableName", "ServiceName", "ControllerName", "ClassName", "ViewName", "RepositoryName", "AppName", "ComponentName", "QueueName", "CacheName", "GatewayName", "OrchestratorName", "WorkerName", "HandlerName", "MiddlewareName", "ExceptionName", "ModuleName", "Key"]) { + if (typeof draft[k] === "string" && (draft[k] as string).length > 0) return draft[k] as string; + } + return null; + }, [draft]); + + // When activeKey is set, render inline — fresh draft + onChange direct + const activeSchema = activeKey ? formSchema.properties?.[activeKey] : null; + const activeResolved = activeSchema ? deref(formSchema, activeSchema) : null; + + const itemCount = (key: string): number => { + const v = draft[key]; + if (Array.isArray(v)) return v.length; + if (v && typeof v === "object") return Object.keys(v).length; + return 0; + }; + + // If sub-page is open, render inline — fresh draft, onChange direct to parent state + if (activeKey && activeResolved) { + const statusText = status === "pending" ? "saving…" + : status === "success" ? "saved" + : status === "error" ? "save failed" + : undefined; + + return ( + setActiveKey(null)} + onSave={() => setActiveKey(null)} + saveDisabled={status === "pending"} + saveStatusText={statusText} + saveStatusTone={status} + > + setAll({ ...draft, [activeKey]: v })} + fieldHints={fieldHints} + pathPrefix={activeKey} + labelOverride="" + sourceNodeId={nodeId} + /> + + ); + } + + return ( +
e.preventDefault()}> + {/* Scalar fields — Name, Description, etc. direct input/textarea */} + {scalarKeys.length > 0 && ( +
+ {scalarKeys.map((key) => { + const propSchema = formSchema.properties![key]; + return ( + setAll({ ...draft, [key]: v })} + fieldHints={fieldHints} + pathPrefix={key} + required={isRequired(formSchema, key)} + sourceNodeId={nodeId} + /> + ); + })} +
+ )} + + {/* Complex fields — array / object → inline subpage */} + {complexKeys.length > 0 && ( +
+ +
+ {complexKeys.map((key) => ( + setActiveKey(key)} + /> + ))} +
+
+ )} + + + + ); +} diff --git a/apps/web/src/components/Inspector/primitives/AddRowButton.tsx b/apps/web/src/components/Inspector/primitives/AddRowButton.tsx new file mode 100644 index 0000000..0137e18 --- /dev/null +++ b/apps/web/src/components/Inspector/primitives/AddRowButton.tsx @@ -0,0 +1,29 @@ +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface Props { + label: string; + onClick: () => void; + size?: "sm" | "md"; +} + +/** "+ label" dashed ghost button — "add" trigger for list container. */ +export function AddRowButton({ label, onClick, size = "md" }: Props) { + return ( + + ); +} diff --git a/apps/web/src/components/Inspector/primitives/ColumnMultiSelect.tsx b/apps/web/src/components/Inspector/primitives/ColumnMultiSelect.tsx new file mode 100644 index 0000000..f67fa0d --- /dev/null +++ b/apps/web/src/components/Inspector/primitives/ColumnMultiSelect.tsx @@ -0,0 +1,58 @@ +/** Multi-column selector — chip-based multi-select from a table's column names. + * Used for Columns/ReferencesColumns in FK/Index/Unique editors. */ + +import { X } from "lucide-react"; +import { Pill } from "./Pill"; +import { Select } from "./Select"; + +interface Props { + /** Selected column names */ + value: string[]; + onChange: (next: string[]) => void; + /** Available column names (columns of this table or the target table) */ + options: string[]; + placeholder?: string; + ariaLabel?: string; +} + +export function ColumnMultiSelect({ value, onChange, options, placeholder, ariaLabel }: Props) { + const remove = (name: string) => onChange(value.filter((v) => v !== name)); + const add = (name: string) => { + if (!name || value.includes(name)) return; + onChange([...value, name]); + }; + const available = options.filter((o) => !value.includes(o)); + + return ( +
+ {value.map((name) => { + const stale = !options.includes(name); + return ( + remove(name)} + title={stale ? "column not found — click to remove" : "remove"} + > + {name} + + + ); + })} + {available.length > 0 && ( + + ); +} diff --git a/apps/web/src/components/Inspector/primitives/index.ts b/apps/web/src/components/Inspector/primitives/index.ts new file mode 100644 index 0000000..361c7f7 --- /dev/null +++ b/apps/web/src/components/Inspector/primitives/index.ts @@ -0,0 +1,33 @@ +/** Inspector primitives — web_old DNA, shared design language for all 21 node types. */ + +export { InspectorShell } from "./InspectorShell"; +export { DrawerShell } from "./DrawerShell"; +export { SubPageShell } from "./SubPageShell"; +export { Field } from "./Field"; +export { Input } from "./Input"; +export { Textarea } from "./Textarea"; +export { Select } from "./Select"; +export { ValueSetSelect } from "./ValueSetSelect"; +export { ValueSetCombobox } from "./ValueSetCombobox"; +export { NodeRefCombobox } from "./NodeRefCombobox"; +export { NodeRefList } from "./NodeRefList"; +export { Switch } from "./Switch"; +export { Pill } from "./Pill"; +export type { PillTone } from "./Pill"; +export { ListRow } from "./ListRow"; +export { ListContainer } from "./ListContainer"; +export { EditGrid } from "./EditGrid"; +export type { GridColumn } from "./EditGrid"; +export { Segmented } from "./Segmented"; +export type { SegOption } from "./Segmented"; +export { ToggleCell } from "./ToggleCell"; +export { AddRowButton } from "./AddRowButton"; +export { MoveButtons } from "./MoveButtons"; +export { DeleteButton } from "./DeleteButton"; +export { IconButton } from "./IconButton"; +export { SectionHeader } from "./SectionHeader"; +export { SaveStatus } from "./SaveStatus"; +export { Eyebrow } from "./Eyebrow"; +export { EmptyHint } from "./EmptyHint"; +export { ColumnMultiSelect } from "./ColumnMultiSelect"; +export { DrawerTrigger } from "./DrawerTrigger"; diff --git a/apps/web/src/components/Inspector/schema-utils.ts b/apps/web/src/components/Inspector/schema-utils.ts new file mode 100644 index 0000000..029cfce --- /dev/null +++ b/apps/web/src/components/Inspector/schema-utils.ts @@ -0,0 +1,210 @@ +/** JSON Schema helpers — for rendering the backend's node-type schema. */ + +export interface JSONSchema { + type?: "string" | "number" | "integer" | "boolean" | "array" | "object" | "null"; + enum?: readonly (string | number | boolean | null)[]; + properties?: Record; + required?: readonly string[]; + items?: JSONSchema; + $ref?: string; + $defs?: Record; + definitions?: Record; + default?: unknown; + description?: string; + minLength?: number; + maxLength?: number; + minItems?: number; + format?: string; +} + +export type WidgetKind = "enum" | "string" | "number" | "boolean" | "array" | "object"; + +/** Resolves a `#/$defs/Foo` or `#/definitions/Foo` style reference within the root. */ +export function resolveRef(root: JSONSchema, ref: string): JSONSchema { + if (!ref.startsWith("#/")) { + throw new Error(`[schema-utils] Unexpected $ref: ${ref}`); + } + const parts = ref.slice(2).split("/"); + let cur: unknown = root; + for (const p of parts) { + if (cur && typeof cur === "object" && p in (cur as Record)) { + cur = (cur as Record)[p]; + } else { + throw new Error(`[schema-utils] $ref target not found: ${ref}`); + } + } + return cur as JSONSchema; +} + +/** Resolves if schema has $ref; returns as-is otherwise. Unwraps chained refs. */ +export function deref(root: JSONSchema, schema: JSONSchema): JSONSchema { + let s = schema; + let safety = 0; + while (s.$ref && safety++ < 16) { + s = resolveRef(root, s.$ref); + } + return s; +} + +/** Widget type for a field. */ +export function getWidgetKind(schema: JSONSchema): WidgetKind { + if (schema.enum && schema.enum.length > 0) return "enum"; + switch (schema.type) { + case "string": return "string"; + case "number": + case "integer": return "number"; + case "boolean": return "boolean"; + case "array": return "array"; + case "object": return "object"; + default: return "string"; + } +} + +/** Generate an empty/default value from schema (for array push). */ +export function defaultForSchema(root: JSONSchema, schema: JSONSchema): unknown { + const s = deref(root, schema); + if (s.default !== undefined) return s.default; + if (s.enum && s.enum.length > 0) return s.enum[0]; + switch (s.type) { + case "string": return ""; + case "number": + case "integer": return 0; + case "boolean": return false; + case "array": return []; + case "object": { + const obj: Record = {}; + if (s.properties) { + for (const [key, prop] of Object.entries(s.properties)) { + obj[key] = defaultForSchema(root, prop); + } + } + return obj; + } + default: return null; + } +} + +/** camelCase / snake_case → "Human Readable" title. */ +export function humanize(key: string): string { + return key + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/_/g, " ") + .replace(/^./, (c) => c.toUpperCase()); +} + +/** Name-like field of an object for array item headers. */ +const NAME_KEYS = [ + "Name", "name", "TableName", "ServiceName", "ControllerName", "FieldName", + "ColumnName", "MethodName", "HandlerName", "RepositoryName", "CacheName", + "QueueName", "GatewayName", "WorkerName", "OrchestratorName", "ComponentName", + "AppName", "MiddlewareName", "ExceptionName", "ModuleName", "ViewName", + "Key", "key", "Label", "label", "Path", "path", "Route", +]; +export function findNameField(obj: Record): string | undefined { + for (const k of NAME_KEYS) { + const v = obj[k]; + if (typeof v === "string" && v.trim()) return v; + } + return undefined; +} + +/** fieldHints lookup — also tries dotted path with indices stripped. + * Backend: "Columns.IsPrimaryKey"; runtime path: "Columns.0.IsPrimaryKey". */ +import type { FieldHint } from "../../api/node-types"; +export function lookupHint( + fieldHints: Record, + fullPath: string, + key: string, +): FieldHint | undefined { + if (fieldHints[fullPath]) return fieldHints[fullPath]; + const stripped = fullPath.split(".").filter((p) => !/^\d+$/.test(p)).join("."); + if (stripped && stripped !== fullPath && fieldHints[stripped]) return fieldHints[stripped]; + if (fieldHints[key]) return fieldHints[key]; + return undefined; +} + +/** Meta fields managed by the backend — not editable in the Inspector. */ +const SYSTEM_FIELDS = new Set([ + "id", "type", "kind", + "createdAt", "updatedAt", "deletedAt", + "homeTabId", "projectId", "tabId", + "x", "y", "position", "Position", + "width", "height", "w", "h", + "isReference", "originTabId", + "_id", "_rev", "_neo4j_id", +]); + +export function isSystemField(key: string): boolean { + return SYSTEM_FIELDS.has(key); +} + +/** String input variant — JSON Schema format + name-pattern fallback. */ +export type StringVariant = "text" | "textarea" | "url" | "email" | "date" | "datetime" | "color" | "code" | "cron" | "sql"; + +/** Variant guess from field name — fallback when backend doesn't provide a format hint. */ +const NAME_TO_VARIANT: Record = { + // cron / schedule + Schedule: "cron", + Cron: "cron", + CronExpression: "cron", + // URL + BaseURL: "url", + URL: "url", + Url: "url", + Endpoint: "url", + CallbackURL: "url", + WebhookURL: "url", + // email + Email: "email", + ContactEmail: "email", + // SQL / kod + Definition: "sql", + Query: "sql", + Sql: "sql", + SQL: "sql", + CustomQuery: "sql", + // regex / pattern (monospace) + Pattern: "code", + Regex: "code", + ValidationPattern: "code", + KeyPattern: "code", + Expression: "code", + // long description + Description: "textarea", + Notes: "textarea", + Documentation: "textarea", + ErrorMessage: "textarea", + // tarih + Date: "date", + StartDate: "date", + EndDate: "date", + CreatedDate: "datetime", + ModifiedDate: "datetime", +}; + +export function getStringVariant(schema: JSONSchema, key: string): StringVariant { + // 1) JSON Schema format is most reliable (backend provided explicitly) + if (schema.format) { + switch (schema.format) { + case "uri": + case "uri-reference": + case "url": + return "url"; + case "email": return "email"; + case "date": return "date"; + case "date-time": return "datetime"; + case "color": return "color"; + case "textarea": return "textarea"; + } + } + // 2) Name-pattern fallback + if (NAME_TO_VARIANT[key]) return NAME_TO_VARIANT[key]; + // 3) Length hint → textarea + if (schema.maxLength != null && schema.maxLength > 200) return "textarea"; + return "text"; +} + +/** Is this key required in the object schema? */ +export function isRequired(parentSchema: JSONSchema, key: string): boolean { + return Array.isArray(parentSchema.required) && parentSchema.required.includes(key); +} diff --git a/apps/web/src/components/Inspector/types/ControllerDrawer.tsx b/apps/web/src/components/Inspector/types/ControllerDrawer.tsx new file mode 100644 index 0000000..dc6f37d --- /dev/null +++ b/apps/web/src/components/Inspector/types/ControllerDrawer.tsx @@ -0,0 +1,349 @@ +import { + SubPageShell, Field, Input, Textarea, SectionHeader, + NodeRefCombobox, NodeRefList, ValueSetCombobox, + EditGrid, Segmented, ToggleCell, type GridColumn, type SegOption, +} from "../primitives"; + +const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"] as const; +type HttpMethod = typeof HTTP_METHODS[number]; + +/** HTTP verb → segmented, her biri kendi --http-* rengiyle (aktif segment renklenir). */ +const HTTP_OPTIONS: readonly SegOption[] = [ + { value: "GET", label: "GET", colorVar: "--http-get" }, + { value: "POST", label: "POST", colorVar: "--http-post" }, + { value: "PUT", label: "PUT", colorVar: "--http-put" }, + { value: "DELETE", label: "DELETE", colorVar: "--http-delete" }, + { value: "PATCH", label: "PATCH", colorVar: "--http-patch" }, +]; + +interface Param { Name: string; Type: string } +interface QueryParam extends Param { Required: boolean } +interface StatusCode { Code: number; Description?: string } +interface RateLimit { Requests: number; WindowSeconds: number } + +interface Endpoint { + HttpMethod: HttpMethod; + Route: string; + RequestDTORef?: string; + ResponseDTORef?: string; + RequiresAuth: boolean; + RequiredRoles: string[]; + PathParams: Param[]; + QueryParams: QueryParam[]; + StatusCodes: StatusCode[]; + MiddlewareRefs: string[]; + RateLimit?: RateLimit; + Description?: string; +} + +const newEndpoint = (): Endpoint => ({ + HttpMethod: "GET", + Route: "/", + RequiresAuth: false, + RequiredRoles: [], + PathParams: [], + QueryParams: [], + StatusCodes: [{ Code: 200, Description: "OK" }], + MiddlewareRefs: [], +}); + +interface Props { + /** ID of the Controller node being edited — for NodeRefCombobox linkAs (USES/RETURNS edge). */ + nodeId: string; + controllerName: string; + baseRoute: string; + properties: Record; + onChange: (next: Record) => void; + saveStatus?: "idle" | "pending" | "success" | "error"; + onBack: () => void; +} + +const ENDPOINT_COLS: readonly GridColumn[] = [ + { key: "method", label: "Method", width: "262px" }, + { key: "route", label: "Route", width: "minmax(140px,1.6fr)" }, + { key: "auth", label: "Auth", width: "44px", align: "center" }, +]; + +export function ControllerDrawer({ nodeId, controllerName, baseRoute, properties, onChange, saveStatus = "idle", onBack }: Props) { + const endpoints = (Array.isArray(properties.Endpoints) ? properties.Endpoints : []) as Endpoint[]; + + const setEndpoints = (next: Endpoint[]) => onChange({ ...properties, Endpoints: next }); + const update = (i: number, patch: Partial) => + setEndpoints(endpoints.map((e, idx) => (idx === i ? { ...e, ...patch } : e))); + const remove = (i: number) => setEndpoints(endpoints.filter((_, idx) => idx !== i)); + const add = () => setEndpoints([...endpoints, newEndpoint()]); + const move = (i: number, dir: -1 | 1) => { + const j = i + dir; + if (j < 0 || j >= endpoints.length) return; + const next = endpoints.slice(); + [next[i], next[j]] = [next[j], next[i]]; + setEndpoints(next); + }; + + const statusText = saveStatus === "pending" ? "saving…" + : saveStatus === "success" ? "saved" + : saveStatus === "error" ? "save failed" + : undefined; + + return ( + + {baseRoute && ( +
+ base + {baseRoute} +
+ )} + String(i)} + addLabel="New endpoint" + emptyLabel="// no endpoints" + onAdd={add} + onMove={move} + onDelete={remove} + renderCell={(ep, key, i) => { + switch (key) { + case "method": + return ( + update(i, { HttpMethod: v as HttpMethod })} + options={HTTP_OPTIONS} + ariaLabel="HTTP method" + /> + ); + case "route": + return ( + update(i, { Route: v })} + placeholder="/:id" + spellCheck={false} + aria-label="Route" + /> + ); + case "auth": + return update(i, { RequiresAuth: v })} ariaLabel="Requires auth" />; + default: + return null; + } + }} + renderDetail={(ep, i) => update(i, patch)} />} + /> +
+ ); +} + +const PATH_COLS: readonly GridColumn[] = [ + { key: "name", label: "Name", width: "minmax(100px,1fr)" }, + { key: "type", label: "Type", width: "minmax(100px,1fr)" }, +]; +const QUERY_COLS: readonly GridColumn[] = [ + { key: "name", label: "Name", width: "minmax(100px,1fr)" }, + { key: "type", label: "Type", width: "minmax(100px,1fr)" }, + { key: "req", label: "Req", width: "38px", align: "center", title: "Required" }, +]; +const STATUS_COLS: readonly GridColumn[] = [ + { key: "code", label: "Code", width: "minmax(96px,0.7fr)" }, + { key: "desc", label: "Description", width: "minmax(120px,1.5fr)" }, +]; + +function EndpointDetail({ + nodeId, endpoint: _ep, update, +}: { nodeId: string; endpoint: Endpoint; update: (patch: Partial) => void }) { + const endpoint: Endpoint = { + ..._ep, + RequiredRoles: _ep.RequiredRoles ?? [], + MiddlewareRefs: _ep.MiddlewareRefs ?? [], + PathParams: _ep.PathParams ?? [], + QueryParams: _ep.QueryParams ?? [], + StatusCodes: _ep.StatusCodes ?? [], + }; + + const setPathParam = (i: number, patch: Partial) => + update({ PathParams: endpoint.PathParams.map((p, idx) => (idx === i ? { ...p, ...patch } : p)) }); + const addPathParam = () => update({ PathParams: [...endpoint.PathParams, { Name: "", Type: "string" }] }); + const delPathParam = (i: number) => update({ PathParams: endpoint.PathParams.filter((_, idx) => idx !== i) }); + + const setQueryParam = (i: number, patch: Partial) => + update({ QueryParams: endpoint.QueryParams.map((p, idx) => (idx === i ? { ...p, ...patch } : p)) }); + const addQueryParam = () => update({ QueryParams: [...endpoint.QueryParams, { Name: "", Type: "string", Required: false }] }); + const delQueryParam = (i: number) => update({ QueryParams: endpoint.QueryParams.filter((_, idx) => idx !== i) }); + + const setStatus = (i: number, patch: Partial) => + update({ StatusCodes: endpoint.StatusCodes.map((s, idx) => (idx === i ? { ...s, ...patch } : s)) }); + const addStatus = () => update({ StatusCodes: [...endpoint.StatusCodes, { Code: 200 }] }); + const delStatus = (i: number) => update({ StatusCodes: endpoint.StatusCodes.filter((_, idx) => idx !== i) }); + + const setListField = (key: "RequiredRoles" | "MiddlewareRefs", raw: string) => { + const next = raw.split(",").map((s) => s.trim()).filter(Boolean); + update({ [key]: next } as Partial); + }; + + return ( + <> +
+ + update({ RequestDTORef: v || undefined })} + placeholder="CreateUserDto" + ariaLabel="Request DTO reference" + linkAs={nodeId ? { sourceNodeId: nodeId, kind: "USES" } : undefined} + /> + + + update({ ResponseDTORef: v || undefined })} + placeholder="UserDto" + ariaLabel="Response DTO reference" + linkAs={nodeId ? { sourceNodeId: nodeId, kind: "RETURNS" } : undefined} + /> + +
+ +
+ + {endpoint.RequiresAuth && ( + + setListField("RequiredRoles", v)} + placeholder="admin, editor" + /> + + )} + + update({ MiddlewareRefs: next })} + nodeType="Middleware" + addLabel="Add Middleware" + /> + +
+ +
+ + String(i)} + addLabel="path param" + emptyLabel="// no path params" + onAdd={addPathParam} + onDelete={delPathParam} + renderCell={(p, key, i) => + key === "name" ? ( + setPathParam(i, { Name: v })} placeholder="id" spellCheck={false} aria-label="Param name" /> + ) : ( + setPathParam(i, { Type: v })} placeholder="number" spellCheck={false} aria-label="Param type" /> + ) + } + /> +
+ +
+ + String(i)} + addLabel="query param" + emptyLabel="// no query params" + onAdd={addQueryParam} + onDelete={delQueryParam} + renderCell={(p, key, i) => { + switch (key) { + case "name": + return setQueryParam(i, { Name: v })} placeholder="page" spellCheck={false} aria-label="Param name" />; + case "type": + return setQueryParam(i, { Type: v })} placeholder="number" spellCheck={false} aria-label="Param type" />; + case "req": + return setQueryParam(i, { Required: v })} ariaLabel="Required" />; + default: + return null; + } + }} + /> +
+ +
+ + String(i)} + addLabel="status code" + emptyLabel="// no status codes" + onAdd={addStatus} + onDelete={delStatus} + renderCell={(s, key, i) => + key === "code" ? ( + { + const n = parseInt(v, 10); + if (!Number.isNaN(n)) setStatus(i, { Code: n }); + }} + placeholder="200" + ariaLabel="HTTP status code" + /> + ) : ( + setStatus(i, { Description: v || undefined })} placeholder="description" aria-label="Status description" /> + ) + } + /> +
+ +
+ +
+ + { + if (v === "") update({ RateLimit: undefined }); + else update({ RateLimit: { Requests: Number(v), WindowSeconds: endpoint.RateLimit?.WindowSeconds ?? 60 } }); + }} + placeholder="100" + /> + + + { + if (v === "") update({ RateLimit: undefined }); + else update({ RateLimit: { Requests: endpoint.RateLimit?.Requests ?? 100, WindowSeconds: Number(v) } }); + }} + placeholder="60" + /> + +
+
+ + +