diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0fb94f4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + tags: ["v*"] + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18, 20, 22] + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Build + run: pnpm build + + - name: Test with coverage + run: pnpm test -- --coverage + + - name: Upload coverage + if: matrix.node-version == 22 + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + + publish: + needs: ci + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + registry-url: https://registry.npmjs.org + + - run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Publish to npm + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/CLAUDE.md b/CLAUDE.md index a78af67..528e99c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Project Overview -npm package that generates 3D interactive maps of TypeScript codebases. Two modes: browser (3D visualization) and MCP stdio (LLM integration). +npm package that analyzes TypeScript codebases — parses source, builds dependency graphs, computes architectural metrics, and exposes everything via MCP stdio for LLM-assisted code understanding. ## Architecture @@ -12,17 +12,20 @@ src/ parser/index.ts <- TS Compiler API parser (files, functions, imports) graph/index.ts <- graphology graph builder + circular dep detection analyzer/index.ts <- Metrics engine (PageRank, betweenness, cohesion, tension, churn, complexity, blast radius, dead exports) - mcp/index.ts <- MCP stdio server (7 tools) - server/index.ts <- Express web server - server/api.ts <- REST API routes + mcp/index.ts <- MCP stdio server (15 tools, 2 prompts, 3 resources) + mcp/hints.ts <- Next-step hints for MCP tool responses + server/graph-store.ts <- Global graph state (shared by CLI + MCP) + impact/index.ts <- Symbol-level impact analysis + rename planning + search/index.ts <- BM25 search engine + process/index.ts <- Entry point detection + call chain tracing + community/index.ts <- Louvain clustering + persistence/index.ts <- Graph export/import to .code-visualizer/ cli.ts <- CLI entry point (commander) -public/ - index.html <- Client-side 3D renderer (8 views, uses 3d-force-graph from CDN) docs/ architecture.md <- Pipeline, module map, data flow, design decisions data-model.md <- All TypeScript interfaces with field descriptions metrics.md <- Per-file + module metrics, force analysis, complexity scoring - mcp-tools.md <- 7 MCP tools: inputs, outputs, use cases, selection guide + mcp-tools.md <- 15 MCP tools: inputs, outputs, use cases, selection guide specs/ active/ <- Current spec ``` @@ -30,7 +33,7 @@ specs/ ## Pipeline ``` -CLI args -> Parser (TS AST) -> Graph Builder (graphology) -> Analyzer (metrics) -> Server (Express | MCP stdio) +CLI args -> Parser (TS AST) -> Graph Builder (graphology) -> Analyzer (metrics) -> MCP stdio ``` ## Key Conventions @@ -47,9 +50,9 @@ CLI args -> Parser (TS AST) -> Graph Builder (graphology) -> Analyzer (metrics) - **graphology** — graph data structure. Import as `import Graph from "graphology"`. Use as both constructor and type. - **graphology-metrics** — PageRank, betweenness. Default imports from subpaths. - **@modelcontextprotocol/sdk** — MCP server. Uses `McpServer` class with `server.tool()` registration. -- **express** — web server. Routes in `server/api.ts`, static files from `public/`. - **typescript** — used as a library (Compiler API), not just a dev tool. - **zod** — MCP tool input validation. +- **commander** — CLI argument parsing. ### Quality Gates @@ -68,7 +71,7 @@ All four must pass before shipping. Run in order: lint -> typecheck -> build -> | Change Type | Bump | Example | |-------------|------|---------| -| New MCP tools, new views, new metrics | minor | 1.0.1 → 1.1.0 | +| New MCP tools, new metrics | minor | 1.0.1 → 1.1.0 | | Bug fixes, description changes, doc sync | patch | 1.1.0 → 1.1.1 | | Breaking: removed tool, changed tool params | major | 1.1.0 → 2.0.0 | @@ -117,23 +120,13 @@ pnpm publish:npm ## Security Rules -### Client-side (public/index.html) -- **NEVER use innerHTML with dynamic data** — use DOM API (`createElement` + `textContent`) -- **NEVER use inline onclick attributes** — use `addEventListener` -- All node data from the API must be treated as untrusted (file paths can contain HTML metacharacters) -- Use the `el()` helper for safe DOM construction - -### Server-side -- Validate and clamp all query parameters (especially `limit`) -- API routes should return JSON 404s, not HTML +- Validate and clamp all MCP tool input parameters - No filesystem access beyond the parsed graph data ## File Conventions - New analysis metrics go in `src/analyzer/index.ts` - New MCP tools go in `src/mcp/index.ts` (register with `server.tool()`) -- New REST endpoints go in `src/server/api.ts` -- New browser views go in `public/index.html` (add render function + view tab) - Types always in `src/types/index.ts` ## Common Pitfalls @@ -153,12 +146,12 @@ LLM knowledge base for building this tool. Single source of truth per topic: | `docs/architecture.md` | Pipeline, module map, data flow, design decisions | New module or pipeline change | | `docs/data-model.md` | All TypeScript interfaces (mirrors `src/types/index.ts`) | Type changes | | `docs/metrics.md` | Per-file + module metrics, force analysis, complexity scoring | New metric added | -| `docs/mcp-tools.md` | 7 MCP tools with inputs/outputs/use cases | New tool or param change | +| `docs/mcp-tools.md` | 15 MCP tools with inputs/outputs/use cases | New tool or param change | ## Testing (BLOCKING) - Test runner: vitest -- Test files: `src/**/*.test.ts` +- Test files: `src/**/*.test.ts`, `tests/**/*.test.ts` - Run: `npm test` or `npx vitest run` ### Coverage Policy (ENFORCE) @@ -171,7 +164,6 @@ LLM knowledge base for building this tool. Single source of truth per topic: ### Real Environment Tests (MANDATORY) - **NEVER mock internal modules** — use real parser, real graph, real analyzer -- **NEVER mock Express** — use `supertest` against the real app - **NEVER mock graphology** — build real graphs with real data - **NEVER mock filesystem for parser tests** — use real fixture directories with real `.ts` files - **Only mock external third-party APIs** that require network/auth (none currently) @@ -184,23 +176,12 @@ LLM knowledge base for building this tool. Single source of truth per topic: | Parser | Real `.ts` fixture files on disk, assert parsed output | | Graph | Real parsed files -> real graph builder, assert nodes/edges | | Analyzer | Real graph -> real metrics, assert values | -| API | `supertest` against real Express app with real graph data | | MCP | Real MCP server instance, assert tool responses | | CLI | Real process execution where feasible | -### Visual Verification (MANDATORY for UI changes) - -- After ANY UI change (HTML/CSS/client JS), start the server and verify in a browser -- Start server: `node dist/cli.js ./src --port 3333` -- Verify: page loads, graph renders, changed feature works visually -- Check browser console for JavaScript errors -- Kill server after verification -- If browser agent is available, use it for automated visual verification - ### Anti-Patterns (NEVER) - NEVER use `jest.mock()` or `vi.mock()` for internal modules - NEVER create fake/stub graph objects — build them through the real pipeline -- NEVER skip tests because "it's just UI" or "it's just config" +- NEVER skip tests because "it's just config" - NEVER write tests that pass regardless of implementation (test behavior, not existence) -- NEVER ship UI changes without visual verification in a real browser diff --git a/README.md b/README.md index 1424c56..81cf659 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # codebase-intelligence -**3D interactive codebase visualization for TypeScript projects.** +**Codebase analysis engine for TypeScript projects.** -Parse your codebase, build a dependency graph, compute architectural metrics, and explore it all in an interactive 3D map. Also works as an MCP server for LLM-assisted code understanding. +Parse your codebase, build a dependency graph, compute architectural metrics, and query it all via MCP for LLM-assisted code understanding. [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen)](https://nodejs.org) @@ -14,20 +14,21 @@ Parse your codebase, build a dependency graph, compute architectural metrics, an --- -## Screenshots - -| Galaxy View | Module View | Forces View | -|:-----------:|:-----------:|:-----------:| -| ![Galaxy](docs/screenshot-galaxy.png) | ![Module](docs/screenshot-module.png) | ![Forces](docs/screenshot-forces.png) | -| 3D force graph, module clouds, group legend | File clusters by directory, labeled clouds | Centrifuge: tension, bridges, candidates | - ## Quick Start +### Claude Code (one-liner) + ```bash -npx codebase-intelligence ./src +claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelligence@latest . ``` -That's it. Opens a 3D map at `http://localhost:3333`. +Done. Available in all projects. Verify with `/mcp` inside Claude Code. + +To scope to a single project instead: + +```bash +claude mcp add -s project -t stdio codebase-intelligence -- npx -y codebase-intelligence@latest ./src +``` ## Table of Contents @@ -35,9 +36,7 @@ That's it. Opens a 3D map at `http://localhost:3333`. - [Installation](#installation) - [Usage](#usage) - [MCP Integration](#mcp-integration) -- [Browser Views](#browser-views) - [Metrics](#metrics) -- [REST API](#rest-api) - [Architecture](#architecture) - [Requirements](#requirements) - [Limitations](#limitations) @@ -46,19 +45,15 @@ That's it. Opens a 3D map at `http://localhost:3333`. ## Features -- **8 interactive 3D views** — Galaxy, Dependency Flow, Hotspot, Focus, Module, Forces, Churn, Coverage +- **15 MCP tools** — codebase overview, file context, hotspots, module structure, force analysis, dead exports, symbol context, search, impact analysis, rename planning, process tracing, community detection, and more +- **2 MCP prompts** — detect_impact, generate_map +- **3 MCP resources** — clusters, processes, setup guide - **11 architectural metrics** — PageRank, betweenness, coupling, cohesion, tension, churn, complexity, blast radius, dead exports, test coverage, escape velocity - **Symbol-level analysis** — call graph with callers/callees, symbol PageRank, per-symbol impact analysis - **BM25 search** — find files and symbols by keyword with ranked results - **Process tracing** — detect entry points and trace execution flows through the call graph - **Community detection** — Louvain algorithm discovers natural file groupings beyond directory structure -- **3D module clouds** — transparent spheres group files by directory with Phong shading and zoom-based fade -- **MCP server** — 15 tools + 2 prompts + 3 resources for LLM-assisted code understanding (Claude Code, Cursor, VS Code) -- **HTTP MCP transport** — Streamable HTTP in browser mode at `POST /api/mcp` -- **REST API** — 13 endpoints for programmatic access -- **Search** — find any file and fly the camera to it -- **Detail panel** — click any node for full metrics -- **Configurable** — node size, link color, physics, cloud opacity +- **Graph persistence** — cache parsed graphs to `.code-visualizer/` for instant startup ## Installation @@ -77,53 +72,24 @@ codebase-intelligence ./src ## Usage -### Browser Mode (default) - ```bash npx codebase-intelligence ./src # => Parsed 142 files, 387 functions, 612 dependencies -# => 3D map ready at http://localhost:3333 -``` - -### MCP Mode - -```bash -npx codebase-intelligence --mcp ./src +# => MCP stdio server started ``` -Starts a stdio MCP server. No browser, no HTTP. - ### Options | Flag | Description | Default | |------|-------------|---------| | `` | Path to TypeScript codebase | required | -| `--mcp` | MCP stdio mode | off | -| `--port ` | Web server port | `3333` | +| `--index` | Persist graph index to `.code-visualizer/` | off | +| `--force` | Re-index even if HEAD unchanged | off | +| `--status` | Print index status and exit | - | +| `--clean` | Remove `.code-visualizer/` index and exit | - | ## MCP Integration -### Claude Code (one-liner) - -```bash -claude mcp add -s user -t stdio codebase-intelligence -- npx -y codebase-intelligence@latest . --mcp -``` - -Done. Available in all projects. Verify with `/mcp` inside Claude Code. - -To scope to a single project instead: - -```bash -claude mcp add -s project -t stdio codebase-intelligence -- npx -y codebase-intelligence@latest ./src --mcp -``` - -### Claude Code (plugin) - -```bash -git clone https://github.com/bntvllnt/claude-plugins.git -claude --plugin-dir ./claude-plugins/plugins/codebase-intelligence -``` - ### Claude Code (manual) Add to `.mcp.json` in your project root: @@ -134,7 +100,7 @@ Add to `.mcp.json` in your project root: "codebase-intelligence": { "type": "stdio", "command": "npx", - "args": ["-y", "codebase-intelligence@latest", "./src", "--mcp"], + "args": ["-y", "codebase-intelligence@latest", "./src"], "env": {} } } @@ -150,7 +116,7 @@ Add to `.cursor/mcp.json` or `.vscode/mcp.json`: "servers": { "codebase-intelligence": { "command": "npx", - "args": ["-y", "codebase-intelligence@latest", "./src", "--mcp"] + "args": ["-y", "codebase-intelligence@latest", "./src"] } } } @@ -176,32 +142,6 @@ Add to `.cursor/mcp.json` or `.vscode/mcp.json`: | `get_processes` | Trace execution flows from entry points through the call graph | | `get_clusters` | Community-detected clusters of related files | -## Browser Views - -| View | What it shows | -|------|---------------| -| **Galaxy** | 3D force-directed graph. Color = module, size = PageRank | -| **Dep Flow** | DAG layout (top-to-bottom). Circular deps in red | -| **Hotspot** | Health heatmap: green (healthy) to red (high coupling) | -| **Focus** | Click a node to see its 2-hop neighborhood | -| **Module** | Files cluster by directory. Cross-module edges in yellow | -| **Forces** | Centrifuge: tension (yellow), bridges (cyan), extraction (green) | -| **Churn** | Git commit frequency heatmap | -| **Coverage** | Test coverage: green = tested, red = untested | - -### Module Clouds - -Transparent 3D spheres group files by top-level directory: - -- Phong shading + wireframe for depth perception -- Zoom-based opacity fade -- Smart grouping: `src/components/ui/` becomes "components" -- Toggle via Settings > "Module Clouds" - -### Group Legend - -Bottom-left legend shows view-specific color coding. When clouds are enabled, adds color swatch + group name + file count + importance % for up to 8 groups sorted by PageRank. - ## Metrics | Metric | What it reveals | @@ -218,24 +158,6 @@ Bottom-left legend shows view-specific color coding. When clouds are enabled, ad | **Dead Exports** | Unused exports (safe to remove) | | **Test Coverage** | Whether a test file exists for each source file | -## REST API - -| Endpoint | Returns | -|----------|---------| -| `GET /api/graph` | All file nodes + edges + stats | -| `GET /api/symbol-graph` | All symbol nodes + call edges + symbol metrics | -| `GET /api/groups` | Group metrics sorted by importance | -| `GET /api/forces` | Force analysis (cohesion, tension, bridges) | -| `GET /api/modules` | Module-level metrics | -| `GET /api/hotspots?metric=coupling&limit=10` | Ranked hotspot files | -| `GET /api/file/` | Single file details + symbol metrics | -| `GET /api/symbols/` | Symbol detail with callers/callees + PageRank | -| `GET /api/search?q=auth&limit=20` | BM25 search results | -| `GET /api/processes` | Entry point traces + execution flows | -| `GET /api/meta` | Project name + staleness info | -| `GET /api/ping` | Health check | -| `POST /api/mcp` | MCP tool invocation (Streamable HTTP transport) | - ## Architecture ``` @@ -243,16 +165,16 @@ codebase-intelligence | v +---------+ +---------+ +----------+ +---------+ - | Parser | --> | Graph | --> | Analyzer | --> | Server | - | TS AST | | grapho- | | metrics | | Next.js | - | | | logy | | | | or MCP | + | Parser | --> | Graph | --> | Analyzer | --> | MCP | + | TS AST | | grapho- | | metrics | | stdio | + | | | logy | | | | | +---------+ +---------+ +----------+ +---------+ ``` 1. **Parser** — extracts files, functions, and imports via the TypeScript Compiler API. Resolves path aliases, respects `.gitignore`, detects test associations. 2. **Graph** — builds nodes and edges with [graphology](https://graphology.github.io/). Detects circular deps via iterative DFS. 3. **Analyzer** — computes all 11 metrics plus group-level aggregations. -4. **Server** — serves the 3D visualization via [Next.js](https://nextjs.org/) + [3d-force-graph](https://github.com/vasturiano/3d-force-graph), or exposes queries via MCP stdio. +4. **MCP** — exposes 15 tools, 2 prompts, and 3 resources via stdio for LLM agents. ## Requirements @@ -264,7 +186,6 @@ codebase-intelligence - TypeScript only (no JS CommonJS, Python, Go, etc.) - Static analysis only (no runtime/dynamic imports) - Call graph confidence varies: type-resolved calls are reliable, text-inferred calls are best-effort -- Client-side 3D requires WebGL ## Release @@ -280,19 +201,14 @@ Publishing is automated and **only happens on `v*` tags**. - `CI` workflow runs on every PR and push to `main`: - lint → typecheck → build → test -### Create a release (auto bump + PR + auto tag) - -1. Open GitHub Actions → `Release PR`. -2. Click **Run workflow** on `main`. -3. Select bump type: `patch` | `minor` | `major`. -4. Merge the generated release PR. +### Create a release -`Release PR` will: -- run lint → typecheck → build → test -- bump `package.json` version -- open a release PR assigned to the workflow runner +1. Bump `package.json` version. +2. Commit: `chore(release): bump to vX.Y.Z` +3. Tag: `git tag vX.Y.Z` +4. Push: `git push origin main --tags` -After merge, `Tag Release` creates and pushes `vX.Y.Z`, which triggers `Publish to npm`. +The `v*` tag triggers the `CI` workflow's **publish** job, which runs `npm publish --access public --provenance`. ## Contributing diff --git a/app/api/file/[...path]/route.ts b/app/api/file/[...path]/route.ts deleted file mode 100644 index 151110f..0000000 --- a/app/api/file/[...path]/route.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET( - _request: Request, - { params }: { params: Promise<{ path: string[] }> }, -): Promise { - return params.then(({ path }) => { - const graph = getGraph(); - const filePath = path.join("/"); - const metrics = graph.fileMetrics.get(filePath); - if (!metrics) { - return NextResponse.json( - { error: `File not found in graph: ${filePath}` }, - { status: 404 }, - ); - } - - const node = graph.nodes.find((n) => n.id === filePath); - const imports = graph.edges.filter((e) => e.source === filePath); - const dependents = graph.edges.filter((e) => e.target === filePath); - const functions = graph.nodes.filter((n) => n.parentFile === filePath); - - return NextResponse.json({ - path: filePath, - loc: node?.loc ?? 0, - module: node?.module ?? "", - functions: functions.map((f) => { - const symId = `${filePath}::${f.label}`; - const symMetrics = graph.symbolMetrics.get(symId); - return { - name: f.label, - loc: f.loc, - fanIn: symMetrics?.fanIn ?? 0, - fanOut: symMetrics?.fanOut ?? 0, - pageRank: symMetrics?.pageRank ?? 0, - }; - }), - imports: imports.map((e) => ({ from: e.target, symbols: e.symbols })), - dependents: dependents.map((e) => ({ - path: e.source, - symbols: e.symbols, - })), - metrics, - }); - }); -} diff --git a/app/api/forces/route.ts b/app/api/forces/route.ts deleted file mode 100644 index 9ddbbdc..0000000 --- a/app/api/forces/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const graph = getGraph(); - return NextResponse.json(graph.forceAnalysis); -} diff --git a/app/api/graph/route.ts b/app/api/graph/route.ts deleted file mode 100644 index 9241a2c..0000000 --- a/app/api/graph/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const graph = getGraph(); - const fileNodes = graph.nodes.filter((n) => n.type === "file"); - const functionNodes = graph.nodes.filter((n) => n.type === "function"); - - const nodes = fileNodes.map((n) => { - const metrics = graph.fileMetrics.get(n.id); - return { - id: n.id, - type: n.type, - label: n.label, - path: n.path, - loc: n.loc, - module: n.module, - pageRank: metrics?.pageRank ?? 0, - betweenness: metrics?.betweenness ?? 0, - coupling: metrics?.coupling ?? 0, - fanIn: metrics?.fanIn ?? 0, - fanOut: metrics?.fanOut ?? 0, - tension: metrics?.tension ?? 0, - isBridge: metrics?.isBridge ?? false, - churn: metrics?.churn ?? 0, - cyclomaticComplexity: metrics?.cyclomaticComplexity ?? 1, - blastRadius: metrics?.blastRadius ?? 0, - deadExports: metrics?.deadExports ?? [], - hasTests: metrics?.hasTests ?? false, - testFile: metrics?.testFile ?? "", - functions: functionNodes - .filter((fn) => fn.parentFile === n.id) - .map((fn) => ({ name: fn.label, loc: fn.loc })), - }; - }); - - const edges = graph.edges.map((e) => ({ - source: e.source, - target: e.target, - symbols: e.symbols, - isTypeOnly: e.isTypeOnly, - weight: e.weight, - })); - - return NextResponse.json({ nodes, edges, stats: graph.stats }); -} diff --git a/app/api/groups/route.ts b/app/api/groups/route.ts deleted file mode 100644 index f7aa726..0000000 --- a/app/api/groups/route.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const graph = getGraph(); - return NextResponse.json({ groups: graph.groups }); -} diff --git a/app/api/hotspots/route.ts b/app/api/hotspots/route.ts deleted file mode 100644 index d344e26..0000000 --- a/app/api/hotspots/route.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(request: Request): NextResponse { - const graph = getGraph(); - const url = new URL(request.url); - const metric = url.searchParams.get("metric") ?? "coupling"; - const limit = Math.min(Math.max(parseInt(url.searchParams.get("limit") ?? "10", 10) || 10, 1), 100); - - const scored: Array<{ path: string; score: number }> = []; - for (const [filePath, metrics] of graph.fileMetrics) { - let score: number; - switch (metric) { - case "coupling": score = metrics.coupling; break; - case "pagerank": score = metrics.pageRank; break; - case "fan_in": score = metrics.fanIn; break; - case "fan_out": score = metrics.fanOut; break; - case "betweenness": score = metrics.betweenness; break; - case "tension": score = metrics.tension; break; - case "churn": score = metrics.churn; break; - case "complexity": score = metrics.cyclomaticComplexity; break; - case "blast_radius": score = metrics.blastRadius; break; - case "coverage": score = metrics.hasTests ? 0 : 1; break; - default: score = 0; - } - scored.push({ path: filePath, score }); - } - - return NextResponse.json({ - metric, - hotspots: scored.sort((a, b) => b.score - a.score).slice(0, limit), - }); -} diff --git a/app/api/mcp/route.ts b/app/api/mcp/route.ts deleted file mode 100644 index ca731c8..0000000 --- a/app/api/mcp/route.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; -import type { CodebaseGraph } from "@/src/types/index"; - -type ToolResult = { content: Array<{ type: "text"; text: string }>; isError?: boolean }; - -function runTool(graph: CodebaseGraph, tool: string, params: Record): ToolResult { - switch (tool) { - case "codebase_overview": { - const modules = [...graph.moduleMetrics.values()].map((m) => ({ - path: m.path, - files: m.files, - loc: m.loc, - avgCoupling: m.cohesion < 0.4 ? "HIGH" : m.cohesion < 0.7 ? "MEDIUM" : "LOW", - cohesion: m.cohesion, - })); - const topDepended = [...graph.fileMetrics.entries()] - .sort(([, a], [, b]) => b.fanIn - a.fanIn) - .slice(0, 5) - .map(([path, m]) => `${path} (${m.fanIn} dependents)`); - const maxDepth = Math.max( - ...graph.nodes.filter((n) => n.type === "file").map((n) => n.path.split("/").length), - ); - const overview = { - totalFiles: graph.stats.totalFiles, - totalFunctions: graph.stats.totalFunctions, - totalDependencies: graph.stats.totalDependencies, - modules: modules.sort((a, b) => b.files - a.files), - topDependedFiles: topDepended, - metrics: { - avgLOC: Math.round( - graph.nodes.filter((n) => n.type === "file").reduce((sum, n) => sum + n.loc, 0) / - graph.stats.totalFiles, - ), - maxDepth, - circularDeps: graph.stats.circularDeps.length, - }, - }; - return { content: [{ type: "text", text: JSON.stringify(overview, null, 2) }] }; - } - - case "file_context": { - const filePath = params.filePath as string; - const metrics = graph.fileMetrics.get(filePath); - if (!metrics) { - return { - content: [{ type: "text", text: JSON.stringify({ error: `File not found: ${filePath}` }) }], - isError: true, - }; - } - const node = graph.nodes.find((n) => n.id === filePath && n.type === "file"); - const fileExports = graph.nodes - .filter((n) => n.parentFile === filePath) - .map((n) => ({ name: n.label, type: "function", loc: n.loc })); - const imports = graph.edges - .filter((e) => e.source === filePath) - .map((e) => ({ from: e.target, symbols: e.symbols })); - const dependents = graph.edges - .filter((e) => e.target === filePath) - .map((e) => ({ path: e.source, symbols: e.symbols })); - const context = { - path: filePath, - loc: node?.loc ?? 0, - exports: fileExports, - imports, - dependents, - metrics: { - pageRank: Math.round(metrics.pageRank * 1000) / 1000, - betweenness: Math.round(metrics.betweenness * 100) / 100, - fanIn: metrics.fanIn, - fanOut: metrics.fanOut, - coupling: Math.round(metrics.coupling * 100) / 100, - tension: metrics.tension, - isBridge: metrics.isBridge, - churn: metrics.churn, - cyclomaticComplexity: metrics.cyclomaticComplexity, - blastRadius: metrics.blastRadius, - deadExports: metrics.deadExports, - hasTests: metrics.hasTests, - testFile: metrics.testFile, - }, - }; - return { content: [{ type: "text", text: JSON.stringify(context, null, 2) }] }; - } - - case "get_dependents": { - const filePath = params.filePath as string; - const depth = (params.depth as number | undefined) ?? 2; - if (!graph.fileMetrics.has(filePath)) { - return { - content: [{ type: "text", text: JSON.stringify({ error: `File not found: ${filePath}` }) }], - isError: true, - }; - } - const directDependents = graph.edges - .filter((e) => e.target === filePath) - .map((e) => ({ path: e.source, symbols: e.symbols })); - const transitive: Array<{ path: string; throughPath: string[]; depth: number }> = []; - const visited = new Set([filePath]); - function bfs(current: string[], currentDepth: number, pathSoFar: string[]): void { - if (currentDepth > depth) return; - const next: string[] = []; - for (const node of current) { - const deps = graph.edges.filter((e) => e.target === node).map((e) => e.source); - for (const dep of deps) { - if (visited.has(dep)) continue; - visited.add(dep); - if (currentDepth > 1) { - transitive.push({ path: dep, throughPath: [...pathSoFar, node], depth: currentDepth }); - } - next.push(dep); - } - } - if (next.length > 0) bfs(next, currentDepth + 1, [...pathSoFar, ...current]); - } - bfs([filePath], 1, []); - const totalAffected = visited.size - 1; - const riskLevel = totalAffected > 20 ? "HIGH" : totalAffected > 5 ? "MEDIUM" : "LOW"; - return { - content: [{ - type: "text", - text: JSON.stringify({ file: filePath, directDependents, transitiveDependents: transitive, totalAffected, riskLevel }, null, 2), - }], - }; - } - - case "find_hotspots": { - const metric = (params.metric as string | undefined) ?? "coupling"; - const limit = (params.limit as number | undefined) ?? 10; - type ScoredFile = { path: string; score: number; reason: string }; - const scored: ScoredFile[] = []; - if (metric === "escape_velocity") { - for (const mod of graph.moduleMetrics.values()) { - scored.push({ - path: mod.path, - score: mod.escapeVelocity, - reason: `${mod.dependedBy.length} modules depend on it, ${mod.externalDeps} external deps`, - }); - } - } else { - for (const [fp, m] of graph.fileMetrics) { - let score = 0; - let reason = ""; - switch (metric) { - case "coupling": score = m.coupling; reason = `fan-in: ${m.fanIn}, fan-out: ${m.fanOut}`; break; - case "pagerank": score = m.pageRank; reason = `${m.fanIn} dependents`; break; - case "fan_in": score = m.fanIn; reason = `${m.fanIn} files import this`; break; - case "fan_out": score = m.fanOut; reason = `imports ${m.fanOut} files`; break; - case "betweenness": score = m.betweenness; reason = m.isBridge ? "bridge" : "on many shortest paths"; break; - case "tension": score = m.tension; reason = score > 0 ? "pulled by multiple modules" : "no tension"; break; - case "churn": score = m.churn; reason = `${m.churn} commits`; break; - case "complexity": score = m.cyclomaticComplexity; reason = `complexity: ${m.cyclomaticComplexity.toFixed(1)}`; break; - case "blast_radius": score = m.blastRadius; reason = `${m.blastRadius} transitive dependents`; break; - case "coverage": score = m.hasTests ? 0 : 1; reason = m.hasTests ? `tested (${m.testFile})` : "no test file"; break; - } - scored.push({ path: fp, score, reason }); - } - } - const hotspots = scored.sort((a, b) => b.score - a.score).slice(0, limit); - const top = hotspots[0]; - const summary = hotspots.length > 0 - ? `Top ${metric} hotspot: ${top.path} (${top.score.toFixed(2)}). ${top.reason}.` - : `No significant ${metric} hotspots found.`; - return { content: [{ type: "text", text: JSON.stringify({ metric, hotspots, summary }, null, 2) }] }; - } - - case "get_module_structure": { - const modules = [...graph.moduleMetrics.values()].map((m) => ({ - path: m.path, files: m.files, loc: m.loc, exports: m.exports, - internalDeps: m.internalDeps, externalDeps: m.externalDeps, - cohesion: m.cohesion, escapeVelocity: m.escapeVelocity, - dependsOn: m.dependsOn, dependedBy: m.dependedBy, - })); - const crossMap = new Map(); - for (const edge of graph.edges) { - const sn = graph.nodes.find((n) => n.id === edge.source); - const tn = graph.nodes.find((n) => n.id === edge.target); - if (!sn || !tn || sn.module === tn.module) continue; - const key = `${sn.module}->${tn.module}`; - crossMap.set(key, (crossMap.get(key) ?? 0) + 1); - } - const crossModuleDeps: Array<{ from: string; to: string; weight: number }> = []; - for (const [key, weight] of crossMap) { - const [from, to] = key.split("->"); - crossModuleDeps.push({ from, to, weight }); - } - return { - content: [{ - type: "text", - text: JSON.stringify({ - modules: modules.sort((a, b) => b.files - a.files), - crossModuleDeps: crossModuleDeps.sort((a, b) => b.weight - a.weight), - circularDeps: graph.stats.circularDeps.map((c) => ({ cycle: c, severity: c.length > 3 ? "HIGH" : "LOW" })), - }, null, 2), - }], - }; - } - - case "analyze_forces": - return { - content: [{ - type: "text", - text: JSON.stringify({ - moduleCohesion: graph.forceAnalysis.moduleCohesion, - tensionFiles: graph.forceAnalysis.tensionFiles, - bridgeFiles: graph.forceAnalysis.bridgeFiles, - extractionCandidates: graph.forceAnalysis.extractionCandidates, - summary: graph.forceAnalysis.summary, - }, null, 2), - }], - }; - - case "get_groups": { - const groups = graph.groups; - if (groups.length === 0) { - return { content: [{ type: "text", text: "No groups found." }] }; - } - const lines = groups.map((g, i) => - `${i + 1}. ${g.name.toUpperCase()} — ${g.files} files, ${g.loc.toLocaleString()} LOC, ` + - `importance: ${(g.importance * 100).toFixed(1)}%, coupling: ${g.fanIn + g.fanOut} (in:${g.fanIn} out:${g.fanOut})`, - ); - return { content: [{ type: "text", text: lines.join("\n") }] }; - } - - case "find_dead_exports": { - const mod = params.module as string | undefined; - const limit = (params.limit as number | undefined) ?? 20; - const deadFiles: Array<{ path: string; module: string; deadExports: string[]; totalExports: number }> = []; - for (const [fp, m] of graph.fileMetrics) { - if (m.deadExports.length === 0) continue; - const node = graph.nodes.find((n) => n.id === fp); - if (!node) continue; - if (mod && node.module !== mod) continue; - deadFiles.push({ - path: fp, - module: node.module, - deadExports: m.deadExports, - totalExports: graph.nodes.filter((n) => n.parentFile === fp).length, - }); - } - const sorted = deadFiles.sort((a, b) => b.deadExports.length - a.deadExports.length).slice(0, limit); - const totalDead = sorted.reduce((sum, f) => sum + f.deadExports.length, 0); - return { - content: [{ - type: "text", - text: JSON.stringify({ - totalDeadExports: totalDead, - files: sorted, - summary: totalDead > 0 - ? `${totalDead} unused exports across ${sorted.length} files.` - : "No dead exports found.", - }, null, 2), - }], - }; - } - - default: - return { - content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${tool}` }) }], - isError: true, - }; - } -} - -export function GET() { - const tools = [ - { name: "codebase_overview", description: "High-level overview of codebase structure, modules, and metrics" }, - { name: "file_context", description: "Detailed context for a specific file", params: ["filePath"] }, - { name: "get_dependents", description: "All files that depend on a given file", params: ["filePath", "depth?"] }, - { name: "find_hotspots", description: "Most problematic files by metric", params: ["metric", "limit?"] }, - { name: "get_module_structure", description: "Module structure with cross-module dependencies" }, - { name: "analyze_forces", description: "Centrifuge force analysis" }, - { name: "find_dead_exports", description: "Unused exports across the codebase", params: ["module?", "limit?"] }, - { name: "get_groups", description: "Top-level directory groups with aggregate metrics" }, - ]; - return NextResponse.json({ tools }); -} - -export async function POST(request: Request) { - try { - const body = (await request.json()) as { tool: string; params?: Record }; - if (!body.tool) { - return NextResponse.json({ error: "Missing 'tool' field" }, { status: 400 }); - } - const graph = getGraph(); - const result = runTool(graph, body.tool, body.params ?? {}); - return NextResponse.json(result, { status: result.isError ? 404 : 200 }); - } catch { - return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); - } -} diff --git a/app/api/meta/route.ts b/app/api/meta/route.ts deleted file mode 100644 index e4abe35..0000000 --- a/app/api/meta/route.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NextResponse } from "next/server"; -import { getProjectName, getIndexedHead } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const indexedHash = getIndexedHead(); - return NextResponse.json({ - projectName: getProjectName(), - staleness: { - stale: false, - indexedHash, - }, - }); -} diff --git a/app/api/modules/route.ts b/app/api/modules/route.ts deleted file mode 100644 index f73ec4d..0000000 --- a/app/api/modules/route.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const graph = getGraph(); - const modules = [...graph.moduleMetrics.values()]; - return NextResponse.json({ modules }); -} diff --git a/app/api/ping/route.ts b/app/api/ping/route.ts deleted file mode 100644 index eb28426..0000000 --- a/app/api/ping/route.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { NextResponse } from "next/server"; - -export function GET(): NextResponse { - return NextResponse.json({ ok: true }); -} diff --git a/app/api/processes/route.ts b/app/api/processes/route.ts deleted file mode 100644 index cdb30db..0000000 --- a/app/api/processes/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; - -export function GET(): NextResponse { - const graph = getGraph(); - - return NextResponse.json({ - processes: graph.processes, - stats: { - totalProcesses: graph.processes.length, - maxDepth: graph.processes.reduce((max, p) => Math.max(max, p.depth), 0), - }, - }); -} diff --git a/app/api/search/route.ts b/app/api/search/route.ts deleted file mode 100644 index b77c8bd..0000000 --- a/app/api/search/route.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; -import { createSearchIndex, search, getSuggestions } from "@/src/search/index"; - -export function GET(request: Request): NextResponse { - const url = new URL(request.url); - const query = url.searchParams.get("q") ?? ""; - const limitParam = url.searchParams.get("limit"); - const limit = limitParam ? Math.min(Math.max(parseInt(limitParam, 10), 1), 100) : 20; - - if (!query) { - return NextResponse.json({ error: "Missing query parameter 'q'" }, { status: 400 }); - } - - const graph = getGraph(); - const index = createSearchIndex(graph); - const results = search(index, query, limit); - - if (results.length === 0) { - const suggestions = getSuggestions(index, query); - return NextResponse.json({ query, results: [], suggestions }); - } - - const mapped = results.map((r) => ({ - file: r.file, - score: r.score, - symbols: r.symbols.map((s) => ({ - name: s.name, - type: s.type, - loc: s.loc, - relevance: s.score, - })), - })); - - return NextResponse.json({ query, results: mapped, suggestions: [] }); -} diff --git a/app/api/symbol-graph/route.ts b/app/api/symbol-graph/route.ts deleted file mode 100644 index fc9ba83..0000000 --- a/app/api/symbol-graph/route.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; -import type { SymbolGraphResponse } from "@/lib/types"; - -export function GET(): NextResponse { - const graph = getGraph(); - - const symbolNodes = graph.symbolNodes.map((s) => { - const metrics = graph.symbolMetrics.get(s.id); - return { - id: s.id, - name: s.name, - type: s.type, - file: s.file, - loc: s.loc, - isDefault: s.isDefault, - fanIn: metrics?.fanIn ?? 0, - fanOut: metrics?.fanOut ?? 0, - pageRank: metrics?.pageRank ?? 0, - betweenness: metrics?.betweenness ?? 0, - }; - }); - - const callEdges = graph.callEdges.map((e) => ({ - source: e.source, - target: e.target, - callerSymbol: e.callerSymbol, - calleeSymbol: e.calleeSymbol, - confidence: e.confidence, - })); - - const symbolMetrics = [...graph.symbolMetrics.values()].map((m) => ({ - symbolId: m.symbolId, - name: m.name, - file: m.file, - fanIn: m.fanIn, - fanOut: m.fanOut, - pageRank: m.pageRank, - betweenness: m.betweenness, - })); - - return NextResponse.json({ symbolNodes, callEdges, symbolMetrics }); -} diff --git a/app/api/symbols/[name]/route.ts b/app/api/symbols/[name]/route.ts deleted file mode 100644 index 79d7334..0000000 --- a/app/api/symbols/[name]/route.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NextResponse } from "next/server"; -import { getGraph } from "@/src/server/graph-store"; -import { getHints } from "@/src/mcp/hints"; - -export function GET( - _request: Request, - { params }: { params: Promise<{ name: string }> }, -): Promise { - return params.then(({ name: symbolName }) => { - const graph = getGraph(); - - const matches = [...graph.symbolMetrics.values()].filter( - (m) => m.name === symbolName || m.symbolId.endsWith(`::${symbolName}`), - ); - - if (matches.length === 0) { - return NextResponse.json( - { error: `Symbol not found: ${symbolName}` }, - { status: 404 }, - ); - } - - const uniqueByFile = new Map(); - for (const m of matches) { - if (!uniqueByFile.has(m.file)) { - uniqueByFile.set(m.file, m); - } - } - let deduped = [...uniqueByFile.values()]; - - if (deduped.length > 1) { - const nonBarrel = deduped.filter((m) => !m.file.endsWith("/index.ts") && m.file !== "index.ts"); - if (nonBarrel.length > 0) deduped = nonBarrel; - } - - if (deduped.length > 1) { - return NextResponse.json({ - disambiguation: deduped.map((m) => ({ - name: m.name, - file: m.file, - symbolId: m.symbolId, - fanIn: m.fanIn, - fanOut: m.fanOut, - })), - }); - } - - const sym = deduped[0]; - const symbolNode = graph.symbolNodes.find((s) => s.id === sym.symbolId); - - const callers = graph.callEdges - .filter((e) => e.calleeSymbol === symbolName || e.target === sym.symbolId) - .map((e) => ({ symbol: e.callerSymbol, file: e.source.split("::")[0] })); - - const callees = graph.callEdges - .filter((e) => e.callerSymbol === symbolName || e.source === sym.symbolId) - .map((e) => ({ symbol: e.calleeSymbol, file: e.target.split("::")[0] })); - - return NextResponse.json({ - name: sym.name, - file: sym.file, - loc: symbolNode?.loc ?? 0, - type: symbolNode?.type ?? "function", - fanIn: sym.fanIn, - fanOut: sym.fanOut, - pageRank: sym.pageRank, - betweenness: sym.betweenness, - callers, - callees, - nextSteps: getHints("symbol_context"), - }); - }); -} diff --git a/app/globals.css b/app/globals.css deleted file mode 100644 index 4606e73..0000000 --- a/app/globals.css +++ /dev/null @@ -1,27 +0,0 @@ -@import "tailwindcss"; - -:root { - --background: #0a0a0f; - --foreground: #e0e0e0; - --muted: #888; - --border: #222; - --accent: #2563eb; - --panel-bg: rgba(15, 15, 25, 0.85); - --panel-bg-solid: rgba(15, 15, 25, 0.95); -} - -@layer base { - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: "Inter", -apple-system, sans-serif; - overflow: hidden; - user-select: none; -} diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 8518d1e..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Metadata } from "next"; -import "./globals.css"; - -export const metadata: Metadata = { - title: "Codebase Intelligence", - description: "3D interactive codebase intelligence", -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}): React.ReactElement { - return ( - - {children} - - ); -} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index f60d700..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { GraphProvider, useGraphContext } from "@/components/graph-provider"; -import { GraphCanvas } from "@/components/graph-canvas"; -import { ProjectBar } from "@/components/project-bar"; -import { ViewTabs } from "@/components/view-tabs"; -import { SearchInput } from "@/components/search-input"; -import { DetailPanel } from "@/components/detail-panel"; -import { SymbolDetailPanel } from "@/components/symbol-detail"; -import { SettingsPanel } from "@/components/settings-panel"; -import { Legend } from "@/components/legend"; -import { FileTree } from "@/components/file-tree"; -import { StaleBanner } from "@/components/stale-banner"; - -function App(): React.ReactElement | null { - const { - graphData, - forceData, - groupData, - symbolData, - projectName, - staleness, - isLoading, - error, - config, - setConfig, - currentView, - setCurrentView, - selectedNode, - setSelectedNode, - selectedSymbol, - setSelectedSymbol, - focusNodeId, - handleNodeClick, - handleNavigate, - handleFocus, - handleSearch, - selectedGroups, - toggleGroup, - } = useGraphContext(); - - const [fileTreeOpen, setFileTreeOpen] = useState(false); - - if (error) { - return ( -
-
- Failed to load graph data: {error.message} -
-
- ); - } - - if (isLoading || !graphData) { - return ( -
-
Loading codebase graph...
-
- ); - } - - return ( - <> - - - - - { setFileTreeOpen((prev) => !prev); }} - /> - - { setSelectedNode(null); }} - onNavigate={handleNavigate} - onFocus={handleFocus} - /> - { setSelectedSymbol(null); }} - /> - - - - ); -} - -export default function Home(): React.ReactElement { - return ( - - - - ); -} diff --git a/components/detail-panel.tsx b/components/detail-panel.tsx deleted file mode 100644 index 2a3aa81..0000000 --- a/components/detail-panel.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client"; - -import type { GraphApiNode, GraphApiEdge } from "@/lib/types"; -import { complexityLabel } from "@/lib/views"; - -function Metric({ label, value, color }: { label: string; value: string; color?: string }): React.ReactElement { - return ( -
- {label} - - {value} - -
- ); -} - -export function DetailPanel({ - node, - edges, - onClose, - onNavigate, - onFocus, -}: { - node: GraphApiNode | null; - edges: GraphApiEdge[]; - onClose: () => void; - onNavigate: (nodeId: string) => void; - onFocus: (nodeId: string) => void; -}): React.ReactElement | null { - if (!node) return null; - - const imports = edges.filter((e) => e.source === node.id); - const dependents = edges.filter((e) => e.target === node.id); - const cxLabel = complexityLabel(node.cyclomaticComplexity); - - return ( -
- -

{node.path}

- - - - - - - - - - - - - - - {node.deadExports.length > 0 && ( - <> -
- Dead Exports ({node.deadExports.length}) -
-
- {node.deadExports.map((name) => ( -
- {name} -
- ))} -
- - )} - - {node.functions.length > 0 && ( - <> -
- Exports ({node.functions.length}) -
-
- {node.functions.map((f) => ( -
- {f.name} ({f.loc} LOC) - {f.fanIn !== undefined && ( - - in:{f.fanIn} out:{f.fanOut ?? 0} PR:{(f.pageRank ?? 0).toFixed(3)} - - )} -
- ))} -
- - )} - -
- Dependencies ({imports.length}) -
-
- {imports.length === 0 ? ( -
None
- ) : ( - imports.map((e) => ( -
{ onNavigate(e.target); }} - > - {e.target} [{e.symbols.join(", ")}] -
- )) - )} -
- -
- Dependents ({dependents.length}) -
-
- {dependents.length === 0 ? ( -
None
- ) : ( - dependents.map((e) => ( -
{ onNavigate(e.source); }} - > - {e.source} [{e.symbols.join(", ")}] -
- )) - )} -
- -
- -
-
- ); -} diff --git a/components/file-tree.tsx b/components/file-tree.tsx deleted file mode 100644 index 7086eba..0000000 --- a/components/file-tree.tsx +++ /dev/null @@ -1,249 +0,0 @@ -"use client"; - -import { useState, useMemo, useCallback, type ChangeEvent } from "react"; -import type { GraphApiNode } from "@/lib/types"; - -interface TreeNode { - name: string; - path: string; - children: TreeNode[]; - fileNode?: GraphApiNode; -} - -function buildTree(nodes: GraphApiNode[]): TreeNode { - const root: TreeNode = { name: "root", path: "", children: [] }; - - for (const node of nodes) { - if (node.type !== "file") continue; - const parts = node.path.split("/"); - let current = root; - - for (let i = 0; i < parts.length; i++) { - const part = parts[i]; - const isFile = i === parts.length - 1; - let child = current.children.find((c) => c.name === part); - - if (!child) { - child = { - name: part, - path: parts.slice(0, i + 1).join("/"), - children: [], - fileNode: isFile ? node : undefined, - }; - current.children.push(child); - } else if (isFile) { - child.fileNode = node; - } - - current = child; - } - } - - sortTree(root); - return root; -} - -function sortTree(node: TreeNode): void { - node.children.sort((a, b) => { - const aIsDir = a.children.length > 0 && !a.fileNode; - const bIsDir = b.children.length > 0 && !b.fileNode; - if (aIsDir && !bIsDir) return -1; - if (!aIsDir && bIsDir) return 1; - return a.name.localeCompare(b.name); - }); - for (const child of node.children) { - sortTree(child); - } -} - -function filterTree(node: TreeNode, query: string): TreeNode | null { - if (!query) return node; - const q = query.toLowerCase(); - - if (node.fileNode && node.path.toLowerCase().includes(q)) { - return { ...node, children: [] }; - } - - const filteredChildren = node.children - .map((c) => filterTree(c, query)) - .filter((c): c is TreeNode => c !== null); - - if (filteredChildren.length > 0) { - return { ...node, children: filteredChildren }; - } - - return null; -} - -function TreeItem({ - node, - depth, - onSelect, - expandedPaths, - onToggle, -}: { - node: TreeNode; - depth: number; - onSelect: (nodeId: string) => void; - expandedPaths: Set; - onToggle: (path: string) => void; -}): React.ReactElement | null { - const isDir = node.children.length > 0; - const isExpanded = expandedPaths.has(node.path); - - if (isDir) { - return ( -
- - {isExpanded && node.children.map((child) => ( - - ))} -
- ); - } - - return ( - - ); -} - -function countFiles(node: TreeNode): number { - if (node.fileNode && node.children.length === 0) return 1; - return node.children.reduce((sum, c) => sum + countFiles(c), 0); -} - -export function FileTree({ - nodes, - onSelect, - isOpen, - onTogglePanel, -}: { - nodes: GraphApiNode[]; - onSelect: (nodeId: string) => void; - isOpen: boolean; - onTogglePanel: () => void; -}): React.ReactElement { - const [filter, setFilter] = useState(""); - const [expandedPaths, setExpandedPaths] = useState>(new Set()); - - const tree = useMemo(() => buildTree(nodes), [nodes]); - const filteredTree = useMemo( - () => (filter ? filterTree(tree, filter) : tree), - [tree, filter], - ); - - const handleToggle = useCallback((path: string) => { - setExpandedPaths((prev) => { - const next = new Set(prev); - if (next.has(path)) { - next.delete(path); - } else { - next.add(path); - } - return next; - }); - }, []); - - const handleExpandAll = useCallback(() => { - const allPaths = new Set(); - function collect(node: TreeNode): void { - if (node.children.length > 0) { - allPaths.add(node.path); - for (const child of node.children) collect(child); - } - } - collect(tree); - setExpandedPaths(allPaths); - }, [tree]); - - const handleCollapseAll = useCallback(() => { - setExpandedPaths(new Set()); - }, []); - - if (!isOpen) { - return ( - - ); - } - - return ( -
-
- Files -
- - - -
-
-
- ) => { setFilter(e.target.value); }} - className="w-full px-2 py-1 text-[10px] bg-transparent text-[#ccc] border border-[#333] rounded outline-none focus:border-[#2563eb]" - /> -
-
- {filteredTree?.children.map((child) => ( - - ))} - {(!filteredTree || filteredTree.children.length === 0) && ( -
No matches
- )} -
-
- ); -} diff --git a/components/graph-canvas.tsx b/components/graph-canvas.tsx deleted file mode 100644 index a478f33..0000000 --- a/components/graph-canvas.tsx +++ /dev/null @@ -1,468 +0,0 @@ -"use client"; - -import { - useRef, - useEffect, - useCallback, - useMemo, - useState, -} from "react"; -import dynamic from "next/dynamic"; -import * as THREE from "three"; -import type { ForceGraph3DInstance } from "3d-force-graph"; -import type { - GraphApiNode, - GraphApiEdge, - GraphConfig, - ForceApiResponse, - ViewType, - SymbolGraphResponse, - SymbolApiNode, -} from "@/lib/types"; -import { - galaxyView, - depFlowView, - hotspotView, - focusView, - moduleView, - forcesView, - churnView, - coverageView, - symbolView, - typesView, - getModuleColor, -} from "@/lib/views"; -import { cloudGroup } from "@/src/cloud-group"; -import { createClusterForce } from "@/lib/cluster-force"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const ForceGraph3D = dynamic(() => import("react-force-graph-3d") as any, { - ssr: false, - loading: () => ( -
- Loading 3D graph... -
- ), -// eslint-disable-next-line @typescript-eslint/no-explicit-any -}) as any; - -interface CloudEntry { - mesh: THREE.Mesh; - wire: THREE.LineSegments; - label: THREE.Sprite; -} - -export function GraphCanvas({ - nodes, - edges, - config, - currentView, - focusNodeId, - forceData, - circularDeps, - symbolData, - selectedGroups, - onNodeClick, - onSymbolClick, -}: { - nodes: GraphApiNode[]; - edges: GraphApiEdge[]; - config: GraphConfig; - currentView: ViewType; - focusNodeId: string | null; - forceData: ForceApiResponse | undefined; - circularDeps: string[][]; - symbolData: SymbolGraphResponse | undefined; - selectedGroups: Set; - onNodeClick: (node: GraphApiNode) => void; - onSymbolClick: (symbol: SymbolApiNode) => void; -}): React.ReactElement { - const fgRef = useRef(undefined); - const cloudsRef = useRef(new Map()); - const configRef = useRef(config); - configRef.current = config; - const containerRef = useRef(null); - // Ref to the node objects passed to ForceGraph3D — the library mutates these in-place with x/y/z - const fgNodesRef = useRef>>([]); - const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); - - const nodeById = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]); - - const graphData = useMemo(() => { - switch (currentView) { - case "galaxy": - return galaxyView(nodes, edges, config); - case "depflow": - return depFlowView(nodes, edges, config, circularDeps); - case "hotspot": - return hotspotView(nodes, edges, config); - case "focus": - return focusView(nodes, edges, config, focusNodeId ?? (nodes[0]?.id || "")); - case "module": - return moduleView(nodes, edges, config, nodeById); - case "forces": - return forcesView( - nodes, edges, config, - forceData ?? { moduleCohesion: [], tensionFiles: [], bridgeFiles: [], extractionCandidates: [], summary: "" }, - nodeById, - ); - case "churn": - return churnView(nodes, edges, config); - case "coverage": - return coverageView(nodes, edges, config); - case "symbols": - return symbolData - ? symbolView(symbolData.symbolNodes, symbolData.callEdges, config) - : { nodes: [], links: [] }; - case "types": - return symbolData - ? typesView(symbolData.symbolNodes, symbolData.callEdges, config) - : { nodes: [], links: [] }; - default: - return galaxyView(nodes, edges, config); - } - }, [nodes, edges, config, currentView, focusNodeId, forceData, circularDeps, nodeById, symbolData]); - - // Build stable node/link objects for ForceGraph3D — store refs for tick handler access - const fgGraphData = useMemo(() => { - const fgNodes = graphData.nodes.map((n) => ({ ...n } as Record)); - const fgLinks = graphData.links.map((l) => ({ ...l })); - fgNodesRef.current = fgNodes; - return { nodes: fgNodes, links: fgLinks }; - }, [graphData]); - - // Window dimensions - useEffect(() => { - function handleResize(): void { - setDimensions({ width: window.innerWidth, height: window.innerHeight }); - } - handleResize(); - window.addEventListener("resize", handleResize); - return () => { window.removeEventListener("resize", handleResize); }; - }, []); - - // Apply physics forces when config changes - useEffect(() => { - const fg = fgRef.current; - if (!fg) return; - const charge = fg.d3Force("charge"); - if (charge && typeof charge.strength === "function") { - charge.strength(config.charge); - } - const link = fg.d3Force("link"); - if (link && typeof link.distance === "function") { - link.distance(config.distance); - } - fg.d3ReheatSimulation(); - }, [config.charge, config.distance]); - - // Cluster force — update strength when slider changes (registration happens in handleEngineTick) - useEffect(() => { - const fg = fgRef.current; - if (!fg) return; - const cluster = fg.d3Force("cluster"); - if (cluster && typeof cluster.strength === "function") { - cluster.strength(config.clusterStrength); - fg.d3ReheatSimulation(); - } - }, [config.clusterStrength]); - - // Module clouds — stable tick handler using refs - const clearClouds = useCallback((fg: ForceGraph3DInstance) => { - if (cloudsRef.current.size === 0) return; - try { - const scene = fg.scene(); - cloudsRef.current.forEach((obj) => { - obj.mesh.geometry.dispose(); - (obj.mesh.material as THREE.Material).dispose(); - scene.remove(obj.mesh); - obj.wire.geometry.dispose(); - (obj.wire.material as THREE.Material).dispose(); - scene.remove(obj.wire); - const spriteMat = obj.label.material; - spriteMat.map?.dispose(); - spriteMat.dispose(); - scene.remove(obj.label); - }); - } catch { /* scene destroyed */ } - cloudsRef.current.clear(); - }, []); - - const handleEngineTick = useCallback(() => { - const fg = fgRef.current; - if (!fg) return; - const cfg = configRef.current; - - // Register cluster force on first tick (fgRef is guaranteed live here) - if (!fg.d3Force("cluster")) { - const cluster = createClusterForce( - (node) => { - const mod = (node.module as string | undefined)?.startsWith(".worktrees/") - ? undefined - : (node.module as string) || undefined; - return mod ? cloudGroup(mod) : undefined; - }, - cfg.clusterStrength, - ); - fg.d3Force("cluster", cluster); - } - - if (!cfg.showModuleBoxes) { - if (cloudsRef.current.size > 0) clearClouds(fg); - if (containerRef.current) containerRef.current.dataset.cloudCount = "0"; - return; - } - - try { - const scene = fg.scene(); - const camera = fg.camera(); - - // Zoom-based opacity: fade clouds when camera is close - const camDist = camera.position.length(); - const fadeNear = 150; - const fadeFar = 500; - const zoomFade = Math.min(1, Math.max(0, (camDist - fadeNear) / (fadeFar - fadeNear))); - - // Read node positions from fgNodesRef — the library mutates these in-place - const fgNodes = fgNodesRef.current; - const groups = new Map>>(); - - fgNodes.forEach((n) => { - if (n.x === undefined) return; - const rawMod = (n.module as string | undefined)?.startsWith(".worktrees/") - ? undefined - : (n.module as string) || "unknown"; - if (!rawMod) return; - const group = cloudGroup(rawMod); - if (!groups.has(group)) groups.set(group, []); - groups.get(group)?.push(n); - }); - - // Dynamic minimum: small projects need fewer files per cloud - const totalNodes = fgNodes.length; - const minFiles = totalNodes > 100 ? 3 : totalNodes > 20 ? 2 : 2; - - const active = new Set(); - groups.forEach((moduleNodes, mod) => { - if (moduleNodes.length < minFiles) return; - active.add(mod); - - let minX = Infinity, minY = Infinity, minZ = Infinity; - let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; - moduleNodes.forEach((n) => { - const x = n.x as number, y = n.y as number, z = n.z as number; - minX = Math.min(minX, x); maxX = Math.max(maxX, x); - minY = Math.min(minY, y); maxY = Math.max(maxY, y); - minZ = Math.min(minZ, z); maxZ = Math.max(maxZ, z); - }); - - const pad = 20; - const rx = Math.max((maxX - minX) / 2 + pad, 12); - const ry = Math.max((maxY - minY) / 2 + pad, 12); - const rz = Math.max((maxZ - minZ) / 2 + pad, 12); - const cx = (minX + maxX) / 2; - const cy = (minY + maxY) / 2; - const cz = (minZ + maxZ) / 2; - - const baseOpacity = cfg.boxOpacity * zoomFade; - const existing = cloudsRef.current.get(mod); - if (existing) { - existing.mesh.position.set(cx, cy, cz); - existing.mesh.scale.set(rx, ry, rz); - (existing.mesh.material as THREE.MeshPhongMaterial).opacity = baseOpacity * 0.3; - existing.wire.position.set(cx, cy, cz); - existing.wire.scale.set(rx, ry, rz); - (existing.wire.material as THREE.LineBasicMaterial).opacity = baseOpacity * 0.5; - existing.label.position.set(cx, maxY + pad + 8, cz); - existing.label.material.opacity = zoomFade; - const labelScale = Math.max(rx, ry, rz) * 1.2; - existing.label.scale.set(labelScale, labelScale * 0.1875, 1); - } else { - const color = getModuleColor(mod); - - // Solid cloud with Phong shading — responds to scene lights - const geo = new THREE.BoxGeometry(2, 2, 2); - const mat = new THREE.MeshPhongMaterial({ - color, - transparent: true, - opacity: baseOpacity * 0.3, - depthWrite: false, - side: THREE.DoubleSide, - shininess: 20, - emissive: new THREE.Color(color), - emissiveIntensity: 0.15, - }); - const mesh = new THREE.Mesh(geo, mat); - mesh.position.set(cx, cy, cz); - mesh.scale.set(rx, ry, rz); - mesh.renderOrder = -1; - scene.add(mesh); - - // Wireframe overlay for 3D depth cues — EdgesGeometry for clean 12-edge box outline - const wireMat = new THREE.LineBasicMaterial({ - color, - transparent: true, - opacity: baseOpacity * 0.5, - depthWrite: false, - }); - const wireframe = new THREE.LineSegments( - new THREE.EdgesGeometry(geo), - wireMat, - ); - wireframe.position.set(cx, cy, cz); - wireframe.scale.set(rx, ry, rz); - wireframe.renderOrder = -1; - scene.add(wireframe); - - // Label: short folder name (last segment) - const shortName = mod.replace(/\/$/, "").split("/").pop() ?? mod; - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - if (ctx) { - canvas.width = 512; canvas.height = 96; - ctx.font = "bold 48px Inter, -apple-system, sans-serif"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.strokeStyle = "#000"; - ctx.lineWidth = 8; - ctx.strokeText(shortName, 256, 48); - ctx.fillStyle = "#fff"; - ctx.fillText(shortName, 256, 48); - } - const texture = new THREE.CanvasTexture(canvas); - const spriteMat = new THREE.SpriteMaterial({ map: texture, transparent: true, depthWrite: false, opacity: zoomFade }); - const label = new THREE.Sprite(spriteMat); - const labelScale = Math.max(rx, ry, rz) * 1.2; - label.scale.set(labelScale, labelScale * 0.1875, 1); - label.position.set(cx, maxY + pad + 8, cz); - scene.add(label); - - cloudsRef.current.set(mod, { mesh, wire: wireframe, label }); - } - }); - - cloudsRef.current.forEach((obj, mod) => { - if (!active.has(mod)) { - obj.mesh.geometry.dispose(); - (obj.mesh.material as THREE.Material).dispose(); - scene.remove(obj.mesh); - obj.wire.geometry.dispose(); - (obj.wire.material as THREE.Material).dispose(); - scene.remove(obj.wire); - const spriteMat = obj.label.material; - spriteMat.map?.dispose(); - spriteMat.dispose(); - scene.remove(obj.label); - cloudsRef.current.delete(mod); - } - }); - - if (containerRef.current) { - containerRef.current.dataset.cloudCount = String(cloudsRef.current.size); - } - } catch { /* scene not ready */ } - }, [clearClouds]); - - // Cleanup clouds on unmount - useEffect(() => { - return () => { - const fg = fgRef.current; - if (fg) clearClouds(fg); - }; - }, [clearClouds]); - - // Search fly: listen for custom event - useEffect(() => { - function handleSearchFly(e: Event): void { - const nodeId = (e as CustomEvent).detail; - const fg = fgRef.current; - if (!fg || !nodeId) return; - const target = fgNodesRef.current.find((n) => n.id === nodeId); - if (target?.x !== undefined) { - fg.cameraPosition( - { x: (target.x as number) + 100, y: (target.y as number) + 100, z: (target.z as number) + 100 }, - { x: target.x as number, y: target.y as number, z: target.z as number }, - 1000, - ); - } - } - window.addEventListener("search-fly", handleSearchFly); - return () => { window.removeEventListener("search-fly", handleSearchFly); }; - }, []); - - const symbolById = useMemo( - () => new Map(symbolData?.symbolNodes.map((s) => [s.id, s]) ?? []), - [symbolData], - ); - - const isSymbolView = currentView === "symbols" || currentView === "types"; - - const handleNodeClick = useCallback( - (node: Record) => { - const id = node.id as string; - if (isSymbolView) { - const sym = symbolById.get(id); - if (sym) onSymbolClick(sym); - } else { - const apiNode = nodeById.get(id); - if (apiNode) onNodeClick(apiNode); - } - }, - [nodeById, symbolById, onNodeClick, onSymbolClick, isSymbolView], - ); - - const showEmptyPlaceholder = - isSymbolView && symbolData?.symbolNodes.length === 0; - - if (showEmptyPlaceholder) { - return ( -
-
No symbols found
-
- ); - } - - return ( -
- ) => - isSymbolView - ? `${n.label as string} (${n.path as string})` - : `${n.path as string} (${n.loc as number} LOC)` - } - nodeColor={(n: Record) => { - if (selectedGroups.size === 0) return n.color as string; - const mod = (n.module as string) || ""; - return selectedGroups.has(cloudGroup(mod)) ? (n.color as string) : "#1a1a24"; - }} - nodeVal={(n: Record) => { - if (selectedGroups.size === 0) return n.size as number; - const mod = (n.module as string) || ""; - return selectedGroups.has(cloudGroup(mod)) ? (n.size as number) : 0.3; - }} - nodeOpacity={config.nodeOpacity} - linkColor={(l: Record) => { - if (selectedGroups.size === 0) return l.color as string; - const src = l.source as Record | string; - const tgt = l.target as Record | string; - const srcMod = typeof src === "object" ? (src.module as string) || "" : ""; - const tgtMod = typeof tgt === "object" ? (tgt.module as string) || "" : ""; - const srcIn = selectedGroups.has(cloudGroup(srcMod)); - const tgtIn = selectedGroups.has(cloudGroup(tgtMod)); - return srcIn && tgtIn ? (l.color as string) : "rgba(30,30,40,0.1)"; - }} - linkWidth={(l: Record) => l.width as number} - linkOpacity={config.linkOpacity} - backgroundColor="#0a0a0f" - onNodeClick={handleNodeClick} - onEngineTick={handleEngineTick} - dagMode={currentView === "depflow" ? "td" : undefined} - dagLevelDistance={currentView === "depflow" ? 50 : undefined} - width={dimensions.width} - height={dimensions.height} - /> -
- ); -} diff --git a/components/graph-provider.tsx b/components/graph-provider.tsx deleted file mode 100644 index 42aa2f1..0000000 --- a/components/graph-provider.tsx +++ /dev/null @@ -1,138 +0,0 @@ -"use client"; - -import { createContext, useContext, useState, useCallback, useEffect } from "react"; -import { useGraphData } from "@/hooks/use-graph-data"; -import { useSymbolData } from "@/hooks/use-symbol-data"; -import { useGraphConfig } from "@/hooks/use-graph-config"; -import type { GraphApiNode, GraphApiResponse, ForceApiResponse, GroupMetrics, GraphConfig, ViewType, SymbolGraphResponse, SymbolApiNode } from "@/lib/types"; - -interface StalenessInfo { - stale: boolean; - indexedHash: string; -} - -interface GraphContextValue { - graphData: GraphApiResponse | undefined; - forceData: ForceApiResponse | undefined; - groupData: GroupMetrics[] | undefined; - symbolData: SymbolGraphResponse | undefined; - projectName: string; - staleness: StalenessInfo | undefined; - isLoading: boolean; - error: Error | undefined; - config: GraphConfig; - setConfig: (key: keyof GraphConfig, value: number | string | boolean) => void; - currentView: ViewType; - setCurrentView: (view: ViewType) => void; - selectedNode: GraphApiNode | null; - setSelectedNode: (node: GraphApiNode | null) => void; - selectedSymbol: SymbolApiNode | null; - setSelectedSymbol: (symbol: SymbolApiNode | null) => void; - focusNodeId: string | null; - setFocusNodeId: (id: string | null) => void; - selectedGroups: Set; - toggleGroup: (name: string) => void; - handleNodeClick: (node: GraphApiNode) => void; - handleNavigate: (nodeId: string) => void; - handleFocus: (nodeId: string) => void; - handleSearch: (nodeId: string) => void; -} - -const GraphContext = createContext(null); - -export function useGraphContext(): GraphContextValue { - const ctx = useContext(GraphContext); - if (!ctx) throw new Error("useGraphContext must be used within GraphProvider"); - return ctx; -} - -export function GraphProvider({ children }: { children: React.ReactNode }) { - const { graphData, forceData, groupData, projectName, staleness, isLoading, error } = useGraphData(); - const { config, setConfig } = useGraphConfig(); - const [currentView, setCurrentView] = useState("galaxy"); - const [selectedNode, setSelectedNode] = useState(null); - const [selectedSymbol, setSelectedSymbol] = useState(null); - const [focusNodeId, setFocusNodeId] = useState(null); - const [selectedGroups, setSelectedGroups] = useState>(new Set()); - const isSymbolView = currentView === "symbols" || currentView === "types"; - const { symbolData } = useSymbolData(isSymbolView); - - useEffect(() => { - if (!isSymbolView) setSelectedSymbol(null); - if (isSymbolView) setSelectedNode(null); - }, [isSymbolView]); - - useEffect(() => { - if (projectName) { - document.title = `${projectName} — Codebase Intelligence`; - } - }, [projectName]); - - const toggleGroup = useCallback((name: string) => { - setSelectedGroups((prev) => { - const next = new Set(prev); - if (next.has(name)) next.delete(name); - else next.add(name); - return next; - }); - }, []); - - const handleNodeClick = useCallback((node: GraphApiNode) => { - setSelectedNode(node); - }, []); - - const handleNavigate = useCallback( - (nodeId: string) => { - const node = graphData?.nodes.find((n) => n.id === nodeId); - if (node) setSelectedNode(node); - }, - [graphData], - ); - - const handleFocus = useCallback((nodeId: string) => { - setFocusNodeId(nodeId); - setCurrentView("focus"); - }, []); - - const handleSearch = useCallback( - (nodeId: string) => { - window.dispatchEvent(new CustomEvent("search-fly", { detail: nodeId })); - const node = graphData?.nodes.find((n) => n.id === nodeId); - if (node) setSelectedNode(node); - }, - [graphData], - ); - - return ( - - {children} - - ); -} diff --git a/components/legend.tsx b/components/legend.tsx deleted file mode 100644 index 0f5b302..0000000 --- a/components/legend.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import type { ViewType, GroupMetrics } from "@/lib/types"; -import { LEGENDS } from "@/lib/views"; - -export function Legend({ - view, - groups, - showClouds, - selectedGroups, - onToggleGroup, -}: { - view: ViewType; - groups: GroupMetrics[] | undefined; - showClouds: boolean; - selectedGroups: Set; - onToggleGroup: (name: string) => void; -}): React.ReactElement { - const items = LEGENDS[view] ?? []; - const showGroups = showClouds && groups && groups.length > 0; - const hasSelection = selectedGroups.size > 0; - - return ( -
{ e.stopPropagation(); }} - > - {items.map((item, i) => ( -
- {item.color && ( - - )} - {item.label} -
- ))} - {showGroups && ( - <> -
-
- Groups {hasSelection && ({selectedGroups.size} selected)} -
- {groups.map((g) => { - const isSelected = selectedGroups.has(g.name); - const dimmed = hasSelection && !isSelected; - return ( - - ); - })} - - )} -
- ); -} diff --git a/components/project-bar.tsx b/components/project-bar.tsx deleted file mode 100644 index fe6220c..0000000 --- a/components/project-bar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import type { GraphApiResponse, ForceApiResponse } from "@/lib/types"; -import { complexityLabel } from "@/lib/views"; - -function Stat({ label, children }: { label: string; children: React.ReactNode }): React.ReactElement { - return ( -
- {label} - {children} -
- ); -} - -export function ProjectBar({ - projectName, - graphData, - forceData, -}: { - projectName: string; - graphData: GraphApiResponse | undefined; - forceData: ForceApiResponse | undefined; -}): React.ReactElement { - if (!graphData) { - return ( -
-

Loading...

-
- ); - } - - const fileNodes = graphData.nodes; - const testedCount = fileNodes.filter((n) => n.hasTests).length; - const totalFiles = fileNodes.length || 1; - const coveragePct = Math.round((testedCount / totalFiles) * 100); - const totalDead = fileNodes.reduce((sum, n) => sum + n.deadExports.length, 0); - const avgComplexity = fileNodes.reduce((sum, n) => sum + n.cyclomaticComplexity, 0) / totalFiles; - const cxLabel = complexityLabel(avgComplexity); - - return ( -
-

{projectName}

- {graphData.stats.totalFiles} - {graphData.stats.totalFunctions} - {graphData.stats.totalDependencies} - {graphData.stats.circularDeps.length} -
- - = 60 ? "#16a34a" : coveragePct >= 30 ? "#ca8a04" : "#dc2626" }}> - {testedCount}/{totalFiles} ({coveragePct}%) - - - {totalDead} - - - {avgComplexity.toFixed(1)} — {cxLabel.text} - - - {forceData?.tensionFiles.length ?? 0} - {forceData?.bridgeFiles.length ?? 0} -
- ); -} diff --git a/components/search-input.tsx b/components/search-input.tsx deleted file mode 100644 index 05c109d..0000000 --- a/components/search-input.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import { useState, useCallback, useRef, useEffect, type ChangeEvent, type KeyboardEvent } from "react"; - -interface SearchResult { - file: string; - score: number; - symbols: Array<{ name: string; type: string; loc: number; relevance: number }>; -} - -export function SearchInput({ - onSearch, -}: { - onSearch: (nodeId: string) => void; -}): React.ReactElement { - const [query, setQuery] = useState(""); - const [results, setResults] = useState([]); - const [isOpen, setIsOpen] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(-1); - const debounceRef = useRef>(undefined); - const containerRef = useRef(null); - - const fetchResults = useCallback(async (q: string) => { - if (!q || q.length < 2) { - setResults([]); - setIsOpen(false); - return; - } - try { - const res = await fetch(`/api/search?q=${encodeURIComponent(q)}&limit=10`); - if (!res.ok) return; - const data = (await res.json()) as { results: SearchResult[] }; - setResults(data.results); - setIsOpen(data.results.length > 0); - setSelectedIndex(-1); - } catch { - /* network error — ignore */ - } - }, []); - - const handleChange = useCallback( - (e: ChangeEvent) => { - const value = e.target.value; - setQuery(value); - clearTimeout(debounceRef.current); - debounceRef.current = setTimeout(() => { - void fetchResults(value); - }, 200); - }, - [fetchResults], - ); - - const handleSelect = useCallback( - (file: string) => { - onSearch(file); - setIsOpen(false); - setQuery(""); - setResults([]); - }, - [onSearch], - ); - - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (!isOpen || results.length === 0) return; - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedIndex((prev) => Math.min(prev + 1, results.length - 1)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedIndex((prev) => Math.max(prev - 1, 0)); - } else if (e.key === "Enter" && selectedIndex >= 0) { - e.preventDefault(); - handleSelect(results[selectedIndex].file); - } else if (e.key === "Escape") { - setIsOpen(false); - } - }, - [isOpen, results, selectedIndex, handleSelect], - ); - - useEffect(() => { - function handleClickOutside(e: MouseEvent): void { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setIsOpen(false); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => { document.removeEventListener("mousedown", handleClickOutside); }; - }, []); - - return ( -
- { if (results.length > 0) setIsOpen(true); }} - className="w-full px-4 py-2 text-xs bg-[rgba(10,10,15,0.85)] text-[#e0e0e0] border border-[#222] rounded-[10px] outline-none backdrop-blur-xl focus:border-[#2563eb]" - /> - {isOpen && results.length > 0 && ( -
- {results.map((r, i) => ( - - ))} -
- )} -
- ); -} diff --git a/components/settings-panel.tsx b/components/settings-panel.tsx deleted file mode 100644 index 62a7323..0000000 --- a/components/settings-panel.tsx +++ /dev/null @@ -1,88 +0,0 @@ -"use client"; - -import type { GraphConfig } from "@/lib/types"; - -function Slider({ - label, - value, - min, - max, - step, - format, - onChange, -}: { - label: string; - value: number; - min: number; - max: number; - step: number; - format: (v: number) => string; - onChange: (v: number) => void; -}): React.ReactElement { - return ( -
- - { onChange(parseFloat(e.target.value)); }} - className="w-full h-1 appearance-none bg-[#333] rounded outline-none [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#2563eb] [&::-webkit-slider-thumb]:cursor-pointer" - /> -
- ); -} - -export function SettingsPanel({ - config, - onChange, -}: { - config: GraphConfig; - onChange: (key: keyof GraphConfig, value: number | string | boolean) => void; -}): React.ReactElement { - return ( -
-
Settings
- -
Nodes
- v.toFixed(2)} onChange={(v) => { onChange("nodeOpacity", v); }} /> - v.toFixed(1)} onChange={(v) => { onChange("nodeSize", v); }} /> - v.toFixed(2)} onChange={(v) => { onChange("isolatedDim", v); }} /> - -
Links
-
- Color - { onChange("linkColor", e.target.value); }} - className="w-8 h-5 border border-[#333] rounded bg-transparent cursor-pointer p-0" - /> -
- v.toFixed(2)} onChange={(v) => { onChange("linkOpacity", v); }} /> - v.toFixed(1)} onChange={(v) => { onChange("linkWidth", v); }} /> - -
Grouping
- - v.toFixed(2)} onChange={(v) => { onChange("boxOpacity", v); }} /> - v.toFixed(2)} onChange={(v) => { onChange("clusterStrength", v); }} /> - -
Physics
- String(Math.round(v))} onChange={(v) => { onChange("charge", v); }} /> - String(Math.round(v))} onChange={(v) => { onChange("distance", v); }} /> -
- ); -} diff --git a/components/stale-banner.tsx b/components/stale-banner.tsx deleted file mode 100644 index 35beadb..0000000 --- a/components/stale-banner.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -interface StalenessInfo { - stale: boolean; - indexedHash: string; -} - -export function StaleBanner({ - staleness, -}: { - staleness: StalenessInfo | undefined; -}): React.ReactElement | null { - if (!staleness?.stale) return null; - - return ( -
- Index may be stale — indexed at {staleness.indexedHash.slice(0, 7)} -
- ); -} diff --git a/components/symbol-detail.tsx b/components/symbol-detail.tsx deleted file mode 100644 index c84c2b7..0000000 --- a/components/symbol-detail.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; - -import type { SymbolApiNode, CallApiEdge } from "@/lib/types"; - -function Metric({ label, value }: { label: string; value: string }): React.ReactElement { - return ( -
- {label} - {value} -
- ); -} - -const TYPE_COLORS: Record = { - function: "#2563eb", - class: "#16a34a", - interface: "#9333ea", - type: "#ea580c", - enum: "#ca8a04", - variable: "#6b7280", -}; - -export function SymbolDetailPanel({ - symbol, - callEdges, - onClose, -}: { - symbol: SymbolApiNode | null; - callEdges: CallApiEdge[]; - onClose: () => void; -}): React.ReactElement | null { - if (!symbol) return null; - - const callers = callEdges - .filter((e) => e.target === symbol.id) - .map((e) => ({ symbol: e.callerSymbol, file: e.source.split("::")[0] })); - - const callees = callEdges - .filter((e) => e.source === symbol.id) - .map((e) => ({ symbol: e.calleeSymbol, file: e.target.split("::")[0] })); - - const typeColor = TYPE_COLORS[symbol.type] ?? "#6b7280"; - - return ( -
- -

{symbol.name}

-
- {symbol.type} -
- - - - - - - -
- Callers ({callers.length}) -
-
- {callers.length === 0 ? ( -
None
- ) : ( - callers.map((c, i) => ( -
- {c.symbol} ({c.file}) -
- )) - )} -
- -
- Callees ({callees.length}) -
-
- {callees.length === 0 ? ( -
None
- ) : ( - callees.map((c, i) => ( -
- {c.symbol} ({c.file}) -
- )) - )} -
-
- ); -} diff --git a/components/view-tabs.tsx b/components/view-tabs.tsx deleted file mode 100644 index fd59e98..0000000 --- a/components/view-tabs.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import type { ViewType } from "@/lib/types"; - -const VIEWS: Array<{ key: ViewType; label: string }> = [ - { key: "galaxy", label: "Galaxy" }, - { key: "depflow", label: "Dep Flow" }, - { key: "hotspot", label: "Hotspot" }, - { key: "focus", label: "Focus" }, - { key: "module", label: "Module" }, - { key: "forces", label: "Forces" }, - { key: "churn", label: "Churn" }, - { key: "coverage", label: "Coverage" }, - { key: "symbols", label: "Symbols" }, - { key: "types", label: "Types" }, -]; - -export function ViewTabs({ - current, - onChange, -}: { - current: ViewType; - onChange: (view: ViewType) => void; -}): React.ReactElement { - return ( -
- {VIEWS.map((v) => ( - - ))} -
- ); -} diff --git a/docs/architecture.md b/docs/architecture.md index 16054b4..0102522 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,8 +18,8 @@ Analyzer | computes: churn, complexity, blast radius, dead exports, test coverage | produces: ForceAnalysis (tension files, bridges, extraction candidates) v -Server (Express) or MCP (stdio) - | serves: REST API + static 3D UI OR MCP tools for LLMs +MCP (stdio) + | exposes: 15 tools, 2 prompts, 3 resources for LLM agents ``` ## Module Map @@ -30,12 +30,15 @@ src/ parser/index.ts <- TS AST extraction + git churn + test detection graph/index.ts <- graphology graph + circular dep detection analyzer/index.ts <- All metric computation - mcp/index.ts <- 7 MCP tools for LLM integration - server/index.ts <- Express setup + port fallback - server/api.ts <- REST API routes (8 endpoints) + mcp/index.ts <- 15 MCP tools for LLM integration + mcp/hints.ts <- Next-step hints for MCP tool responses + impact/index.ts <- Symbol-level impact analysis + rename planning + search/index.ts <- BM25 search engine + process/index.ts <- Entry point detection + call chain tracing + community/index.ts <- Louvain clustering + persistence/index.ts <- Graph export/import to .code-visualizer/ + server/graph-store.ts <- Global graph state (shared by CLI + MCP) cli.ts <- Entry point, wires pipeline together -public/ - index.html <- 8-view 3D client (3d-force-graph + Three.js) ``` ## Data Flow @@ -49,23 +52,23 @@ buildGraph(parsedFiles) analyzeGraph(builtGraph, parsedFiles) -> CodebaseGraph { - nodes, edges, fileMetrics, moduleMetrics, forceAnalysis, stats + nodes, edges, symbolNodes, callEdges, symbolMetrics, + fileMetrics, moduleMetrics, forceAnalysis, stats, + groups, processes, clusters } -startServer(codebaseGraph, port, projectName) - -> Express app serving /api/* + static /public/ - startMcpServer(codebaseGraph) - -> stdio MCP server with 7 tools + -> stdio MCP server with 15 tools, 2 prompts, 3 resources ``` ## Key Design Decisions -- **Single HTML file**: No build step for client. CDN for 3d-force-graph + Three.js. Keeps iteration fast. +- **MCP-only**: No web UI or REST API. All interaction through MCP stdio for LLM agents. - **graphology**: In-memory graph with O(1) neighbor lookup. PageRank and betweenness computed via graphology-metrics. - **Batch git churn**: Single `git log --all --name-only` call, parsed for all files. Avoids O(n) subprocess spawning. - **Dead export detection**: Cross-references parsed exports against edge symbol lists. May miss `import *` or re-exports (known limitation). - **Graceful degradation**: Non-git dirs get churn=0, no-test codebases get coverage=false. Never crashes. +- **Graph persistence**: Optional `--index` flag caches parsed graph to `.code-visualizer/` for instant startup on unchanged HEAD. ## Adding a New Metric @@ -75,6 +78,4 @@ Vertical slice through all layers: 2. **parser/index.ts** — Extract raw data from AST or external source (git, filesystem) 3. **analyzer/index.ts** — Compute derived metric, store in `fileMetrics` map 4. **mcp/index.ts** — Expose via `find_hotspots` enum or new tool -5. **server/api.ts** — Add to `/graph` node shape and `/hotspots` switch -6. **public/index.html** — Add view tab + render function + detail panel row + legend -7. **Tests** — Cover parser extraction + analyzer computation +5. **Tests** — Cover parser extraction + analyzer computation diff --git a/docs/mcp-tools.md b/docs/mcp-tools.md index 7667daf..2449334 100644 --- a/docs/mcp-tools.md +++ b/docs/mcp-tools.md @@ -1,6 +1,6 @@ # MCP Tools Reference -15 tools available via `--mcp` mode (stdio) or HTTP transport (`POST /api/mcp`). +15 tools available via MCP stdio. ## 1. codebase_overview diff --git a/docs/screenshot-forces.png b/docs/screenshot-forces.png deleted file mode 100644 index fb07a1f..0000000 Binary files a/docs/screenshot-forces.png and /dev/null differ diff --git a/docs/screenshot-galaxy.png b/docs/screenshot-galaxy.png deleted file mode 100644 index e77d38d..0000000 Binary files a/docs/screenshot-galaxy.png and /dev/null differ diff --git a/docs/screenshot-module.png b/docs/screenshot-module.png deleted file mode 100644 index adc3ab1..0000000 Binary files a/docs/screenshot-module.png and /dev/null differ diff --git a/e2e/api.spec.ts b/e2e/api.spec.ts deleted file mode 100644 index d6c0f14..0000000 --- a/e2e/api.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("API Routes", () => { - test("GET /api/ping returns ok", async ({ request }) => { - const res = await request.get("/api/ping"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body).toEqual({ ok: true }); - }); - - test("GET /api/meta returns project name", async ({ request }) => { - const res = await request.get("/api/meta"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.projectName).toBe("e2e-fixture-project"); - }); - - test("GET /api/graph returns nodes, edges, stats", async ({ request }) => { - const res = await request.get("/api/graph"); - expect(res.status()).toBe(200); - const body = await res.json(); - - expect(Array.isArray(body.nodes)).toBe(true); - expect(Array.isArray(body.edges)).toBe(true); - expect(body.stats).toBeDefined(); - expect(body.stats.totalFiles).toBe(10); - expect(body.stats.totalFunctions).toBeGreaterThan(0); - expect(body.stats.totalDependencies).toBeGreaterThan(0); - expect(Array.isArray(body.stats.circularDeps)).toBe(true); - expect(body.stats.circularDeps.length).toBeGreaterThan(0); - - // Verify node shape - const fileNode = body.nodes.find((n: { type: string }) => n.type === "file"); - expect(fileNode).toBeDefined(); - expect(fileNode.id).toBeDefined(); - expect(fileNode.path).toBeDefined(); - expect(fileNode.module).toBeDefined(); - expect(typeof fileNode.loc).toBe("number"); - expect(typeof fileNode.pageRank).toBe("number"); - expect(typeof fileNode.betweenness).toBe("number"); - expect(typeof fileNode.coupling).toBe("number"); - expect(typeof fileNode.fanIn).toBe("number"); - expect(typeof fileNode.fanOut).toBe("number"); - expect(typeof fileNode.churn).toBe("number"); - expect(typeof fileNode.cyclomaticComplexity).toBe("number"); - expect(typeof fileNode.blastRadius).toBe("number"); - expect(Array.isArray(fileNode.deadExports)).toBe(true); - expect(typeof fileNode.hasTests).toBe("boolean"); - expect(Array.isArray(fileNode.functions)).toBe(true); - }); - - test("GET /api/modules returns module structure", async ({ request }) => { - const res = await request.get("/api/modules"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(Array.isArray(body.modules)).toBe(true); - expect(body.modules.length).toBeGreaterThan(0); - - const mod = body.modules[0]; - expect(mod.path).toBeDefined(); - expect(typeof mod.files).toBe("number"); - expect(typeof mod.cohesion).toBe("number"); - }); - - test("GET /api/forces returns force analysis", async ({ request }) => { - const res = await request.get("/api/forces"); - expect(res.status()).toBe(200); - const body = await res.json(); - - expect(Array.isArray(body.moduleCohesion)).toBe(true); - expect(Array.isArray(body.tensionFiles)).toBe(true); - expect(Array.isArray(body.bridgeFiles)).toBe(true); - expect(Array.isArray(body.extractionCandidates)).toBe(true); - expect(typeof body.summary).toBe("string"); - }); - - test("GET /api/hotspots returns hotspots (default)", async ({ request }) => { - const res = await request.get("/api/hotspots"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.metric).toBe("coupling"); - expect(Array.isArray(body.hotspots)).toBe(true); - }); - - test("GET /api/hotspots?metric=complexity returns sorted results", async ({ request }) => { - const res = await request.get("/api/hotspots?metric=complexity&limit=5"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.metric).toBe("complexity"); - expect(Array.isArray(body.hotspots)).toBe(true); - expect(body.hotspots.length).toBeLessThanOrEqual(5); - }); - - test("GET /api/file/[path] returns file details", async ({ request }) => { - // Use a file path from the fixture project - const graphRes = await request.get("/api/graph"); - const graph = await graphRes.json(); - const firstFile = graph.nodes.find((n: { type: string }) => n.type === "file"); - expect(firstFile).toBeDefined(); - - const res = await request.get(`/api/file/${firstFile.id}`); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.path).toBe(firstFile.id); - expect(typeof body.loc).toBe("number"); - expect(Array.isArray(body.functions)).toBe(true); - expect(Array.isArray(body.imports)).toBe(true); - expect(Array.isArray(body.dependents)).toBe(true); - expect(body.metrics).toBeDefined(); - }); - - test("GET /api/file/nonexistent returns 404", async ({ request }) => { - const res = await request.get("/api/file/this/does/not/exist.ts"); - expect(res.status()).toBe(404); - const body = await res.json(); - expect(body.error).toBeDefined(); - }); -}); diff --git a/e2e/fixture-project/package.json b/e2e/fixture-project/package.json deleted file mode 100644 index 0858f1b..0000000 --- a/e2e/fixture-project/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "e2e-fixture-project", - "version": "1.0.0", - "type": "module" -} diff --git a/e2e/fixture-project/src/empty.ts b/e2e/fixture-project/src/empty.ts deleted file mode 100644 index 0fee456..0000000 --- a/e2e/fixture-project/src/empty.ts +++ /dev/null @@ -1 +0,0 @@ -/** Empty file — edge case for parser */ diff --git a/e2e/fixture-project/src/index.ts b/e2e/fixture-project/src/index.ts deleted file mode 100644 index adf85b2..0000000 --- a/e2e/fixture-project/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** Entry point — barrel file re-exporting from submodules */ -export { createUser, deleteUser } from "./services/user-service.js"; -export { formatDate } from "./utils/format.js"; -export type { User } from "./models/user.js"; -export { fetchData } from "./services/api-client.js"; diff --git a/e2e/fixture-project/src/models/config.ts b/e2e/fixture-project/src/models/config.ts deleted file mode 100644 index 4ad2c71..0000000 --- a/e2e/fixture-project/src/models/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** Config model — dead exports (unused by anything) */ -export interface AppConfig { - port: number; - debug: boolean; - logLevel: "info" | "warn" | "error"; -} - -export const DEFAULT_CONFIG: AppConfig = { - port: 3000, - debug: false, - logLevel: "info", -}; - -/** Completely unused function — dead export */ -export function validateConfig(config: AppConfig): boolean { - return config.port > 0 && config.port < 65536; -} - -/** Another dead export */ -export function mergeConfig(base: AppConfig, override: Partial): AppConfig { - return { ...base, ...override }; -} diff --git a/e2e/fixture-project/src/models/user.ts b/e2e/fixture-project/src/models/user.ts deleted file mode 100644 index 0fe7f50..0000000 --- a/e2e/fixture-project/src/models/user.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** User model — type-only file (no runtime exports) */ -export interface User { - id: string; - name: string; - email: string; - role: "admin" | "user" | "guest"; - createdAt: Date; -} - -export interface UserCreateInput { - name: string; - email: string; - role?: User["role"]; -} - -export type UserId = string; diff --git a/e2e/fixture-project/src/services/api-client.ts b/e2e/fixture-project/src/services/api-client.ts deleted file mode 100644 index fbff6b4..0000000 --- a/e2e/fixture-project/src/services/api-client.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** API client — creates circular dependency with auth */ -import { getToken } from "./auth.js"; - -export async function fetchData(url: string): Promise { - const token = getToken(); - const headers: Record = {}; - if (token) { - headers["Authorization"] = `Bearer ${token}`; - } - return { url, headers, mock: true }; -} - -export async function postData(url: string, body: unknown): Promise { - const token = getToken(); - return { url, body, token, mock: true }; -} - -/** Dead export */ -export function buildUrl(base: string, path: string): string { - return `${base}/${path}`; -} diff --git a/e2e/fixture-project/src/services/auth.ts b/e2e/fixture-project/src/services/auth.ts deleted file mode 100644 index 8b4abdc..0000000 --- a/e2e/fixture-project/src/services/auth.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** Auth service — circular dep with api-client */ -import { postData } from "./api-client.js"; - -let currentToken: string | null = null; - -export function getToken(): string | null { - return currentToken; -} - -export async function login(email: string, password: string): Promise { - const result = await postData("/auth/login", { email, password }); - if (result) { - currentToken = "mock-token-" + Date.now(); - return true; - } - return false; -} - -export function logout(): void { - currentToken = null; -} diff --git a/e2e/fixture-project/src/services/user-service.ts b/e2e/fixture-project/src/services/user-service.ts deleted file mode 100644 index 5c75e59..0000000 --- a/e2e/fixture-project/src/services/user-service.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** User service — high complexity, many branches */ -import type { User, UserCreateInput, UserId } from "../models/user.js"; -import { formatDate, slugify } from "../utils/format.js"; -import { fetchData } from "./api-client.js"; - -const users: Map = new Map(); - -export function createUser(input: UserCreateInput): User { - if (!input.name || input.name.trim().length === 0) { - throw new Error("Name is required"); - } - if (!input.email || !input.email.includes("@")) { - throw new Error("Valid email is required"); - } - - const role = input.role ?? "user"; - const id = slugify(input.name) + "-" + Date.now(); - - if (role === "admin") { - const existing = Array.from(users.values()).filter((u) => u.role === "admin"); - if (existing.length >= 3) { - throw new Error("Maximum 3 admins allowed"); - } - } - - const user: User = { - id, - name: input.name.trim(), - email: input.email.toLowerCase(), - role, - createdAt: new Date(), - }; - - users.set(id, user); - return user; -} - -export function deleteUser(id: UserId): boolean { - return users.delete(id); -} - -export function findUser(id: UserId): User | undefined { - return users.get(id); -} - -export function listUsers(role?: User["role"]): User[] { - const all = Array.from(users.values()); - if (!role) return all; - return all.filter((u) => u.role === role); -} - -/** High cyclomatic complexity — many conditions */ -export function processUser(user: User, action: string): string { - const date = formatDate(user.createdAt); - - if (action === "activate") { - if (user.role === "admin") return `Admin ${user.name} activated on ${date}`; - if (user.role === "user") return `User ${user.name} activated on ${date}`; - if (user.role === "guest") return `Guest ${user.name} activated on ${date}`; - return `Unknown role for ${user.name}`; - } else if (action === "deactivate") { - if (user.role === "admin") return `Cannot deactivate admin ${user.name}`; - return `${user.name} deactivated`; - } else if (action === "promote") { - if (user.role === "guest") return `${user.name} promoted to user`; - if (user.role === "user") return `${user.name} promoted to admin`; - return `${user.name} already admin`; - } else if (action === "export") { - void fetchData(`/users/${user.id}/export`); - return `Exporting ${user.name}`; - } - return `Unknown action: ${action}`; -} - -/** Dead export — unused */ -export function getUserCount(): number { - return users.size; -} diff --git a/e2e/fixture-project/src/types/index.ts b/e2e/fixture-project/src/types/index.ts deleted file mode 100644 index e3f1573..0000000 --- a/e2e/fixture-project/src/types/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** Type-only barrel — no runtime code */ -export type { User, UserCreateInput, UserId } from "../models/user.js"; -export type { AppConfig } from "../models/config.js"; diff --git a/e2e/fixture-project/src/utils/format.ts b/e2e/fixture-project/src/utils/format.ts deleted file mode 100644 index f28d1cf..0000000 --- a/e2e/fixture-project/src/utils/format.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** Format utilities — used by multiple services */ -export function formatDate(date: Date): string { - return date.toISOString().split("T")[0] ?? ""; -} - -export function slugify(text: string): string { - return text - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-|-$/g, ""); -} - -export function truncate(text: string, max: number): string { - if (text.length <= max) return text; - return text.slice(0, max) + "..."; -} - -/** Dead export */ -export function capitalize(text: string): string { - return text.charAt(0).toUpperCase() + text.slice(1); -} diff --git a/e2e/fixture-project/src/utils/validators.ts b/e2e/fixture-project/src/utils/validators.ts deleted file mode 100644 index aea0df1..0000000 --- a/e2e/fixture-project/src/utils/validators.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** Validators — deep nesting, high complexity */ -import type { User } from "../models/user.js"; - -export function validateEmail(email: string): { valid: boolean; reason?: string } { - if (!email) return { valid: false, reason: "empty" }; - if (!email.includes("@")) return { valid: false, reason: "missing @" }; - if (email.startsWith("@")) return { valid: false, reason: "starts with @" }; - if (email.endsWith("@")) return { valid: false, reason: "ends with @" }; - const parts = email.split("@"); - if (parts.length !== 2) return { valid: false, reason: "multiple @" }; - if (!parts[1]?.includes(".")) return { valid: false, reason: "no domain TLD" }; - return { valid: true }; -} - -export function validateUser(user: Partial): string[] { - const errors: string[] = []; - if (!user.name) errors.push("name required"); - if (!user.email) { - errors.push("email required"); - } else { - const result = validateEmail(user.email); - if (!result.valid) errors.push(`email invalid: ${result.reason}`); - } - if (user.role && !["admin", "user", "guest"].includes(user.role)) { - errors.push("invalid role"); - } - return errors; -} - -/** Unused */ -export function isStrongPassword(password: string): boolean { - return password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password); -} diff --git a/e2e/fixture-project/tsconfig.json b/e2e/fixture-project/tsconfig.json deleted file mode 100644 index f338619..0000000 --- a/e2e/fixture-project/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "outDir": "dist", - "rootDir": "src" - }, - "include": ["src"] -} diff --git a/e2e/groups.spec.ts b/e2e/groups.spec.ts deleted file mode 100644 index aadb35f..0000000 --- a/e2e/groups.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Group Selection", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - await page.waitForSelector("canvas", { timeout: 15_000 }); - }); - - test("legend shows Groups section with group names", async ({ page }) => { - await expect(page.getByText("Groups", { exact: false })).toBeVisible(); - - // Fixture has 5 groups: services, models, utils, src, types - const groupButtons = page.locator("button[data-group]"); - await expect(groupButtons).toHaveCount(5); - - const names = await groupButtons.locator("span.truncate").allTextContents(); - expect(names).toContain("services"); - expect(names).toContain("models"); - expect(names).toContain("utils"); - }); - - test("clicking a group selects it and shows selection count", async ({ page }) => { - const servicesBtn = page.locator("button[data-group='services']"); - await expect(servicesBtn).toBeVisible(); - - await servicesBtn.click(); - - // Header should show "(1 selected)" - await expect(page.getByText("1 selected")).toBeVisible(); - - // The clicked button should have highlighted background - await expect(servicesBtn).not.toHaveClass(/opacity-40/); - - // Other groups should be dimmed - const modelsBtn = page.locator("button[data-group='models']"); - await expect(modelsBtn).toHaveClass(/opacity-40/); - }); - - test("clicking a selected group deselects it", async ({ page }) => { - const servicesBtn = page.locator("button[data-group='services']"); - - // Select - await servicesBtn.click(); - await expect(page.getByText("1 selected")).toBeVisible(); - - // Deselect - await servicesBtn.click(); - - // "selected" text should be gone - await expect(page.getByText("selected")).not.toBeVisible(); - - // No group should be dimmed - const allButtons = page.locator("button[data-group]"); - const count = await allButtons.count(); - for (let i = 0; i < count; i++) { - await expect(allButtons.nth(i)).not.toHaveClass(/opacity-40/); - } - }); - - test("multi-select: clicking two groups selects both", async ({ page }) => { - const servicesBtn = page.locator("button[data-group='services']"); - const modelsBtn = page.locator("button[data-group='models']"); - - await servicesBtn.click(); - await modelsBtn.click(); - - await expect(page.getByText("2 selected")).toBeVisible(); - - // Both should not be dimmed - await expect(servicesBtn).not.toHaveClass(/opacity-40/); - await expect(modelsBtn).not.toHaveClass(/opacity-40/); - - // Others should be dimmed - const utilsBtn = page.locator("button[data-group='utils']"); - await expect(utilsBtn).toHaveClass(/opacity-40/); - }); - - test("deselecting one of two selected groups keeps the other", async ({ page }) => { - const servicesBtn = page.locator("button[data-group='services']"); - const modelsBtn = page.locator("button[data-group='models']"); - - await servicesBtn.click(); - await modelsBtn.click(); - await expect(page.getByText("2 selected")).toBeVisible(); - - // Deselect services - await servicesBtn.click(); - await expect(page.getByText("1 selected")).toBeVisible(); - - // Services should now be dimmed, models still selected - await expect(servicesBtn).toHaveClass(/opacity-40/); - await expect(modelsBtn).not.toHaveClass(/opacity-40/); - }); - - test("group buttons have correct data-group attributes", async ({ page }) => { - const groupButtons = page.locator("button[data-group]"); - const attrs = await groupButtons.evaluateAll( - (els) => els.map((el) => el.getAttribute("data-group")), - ); - expect(attrs.length).toBe(5); - expect(attrs).toEqual(expect.arrayContaining(["services", "models", "utils", "src", "types"])); - }); - - test("cluster strength slider exists and is adjustable", async ({ page }) => { - const slider = page.locator("input[type='range']").last(); - await expect(slider).toBeVisible(); - - // The settings panel should show "Cluster Strength" label - await expect(page.getByText("Cluster Strength")).toBeVisible(); - - // Adjust the slider value - await slider.fill("0.8"); - await expect(slider).toHaveValue("0.8"); - - // Set to 0 (off) - await slider.fill("0"); - await expect(slider).toHaveValue("0"); - }); - - test("groups only visible when Module Clouds checkbox is checked", async ({ page }) => { - // Groups should be visible initially (checkbox is checked by default) - await expect(page.getByText("Groups", { exact: false })).toBeVisible(); - - // Uncheck Module Clouds - const checkbox = page.locator("input[type='checkbox']"); - await checkbox.uncheck(); - - // Groups section should disappear - await expect(page.locator("button[data-group]").first()).not.toBeVisible(); - }); -}); diff --git a/e2e/interactions.spec.ts b/e2e/interactions.spec.ts deleted file mode 100644 index cf229dc..0000000 --- a/e2e/interactions.spec.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("UI Interactions", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - await page.waitForSelector("canvas", { timeout: 15_000 }); - }); - - test("search input exists and accepts text", async ({ page }) => { - const search = page.locator("input[placeholder='Search files...']"); - await expect(search).toBeVisible(); - await search.fill("user"); - await expect(search).toHaveValue("user"); - }); - - test("settings sliders are interactive", async ({ page }) => { - const opacitySlider = page.locator("text=Opacity").first().locator("..").locator("input[type='range']"); - await expect(opacitySlider).toBeVisible(); - - const initialValue = await opacitySlider.inputValue(); - expect(Number(initialValue)).toBeGreaterThan(0); - }); - - test("settings panel shows all sections", async ({ page }) => { - await expect(page.locator("text=NODES")).toBeVisible(); - await expect(page.locator("text=LINKS")).toBeVisible(); - await expect(page.locator("text=GROUPING")).toBeVisible(); - await expect(page.locator("text=PHYSICS")).toBeVisible(); - }); - - test("module clouds checkbox is interactive", async ({ page }) => { - const checkbox = page.locator("input[type='checkbox']"); - await expect(checkbox).toBeVisible(); - const checked = await checkbox.isChecked(); - expect(typeof checked).toBe("boolean"); - }); - - test("module clouds are visible when checkbox is checked", async ({ page }) => { - const checkbox = page.locator("input[type='checkbox']"); - await expect(checkbox).toBeChecked(); - - // Wait for clouds to render (engine needs ticks to position nodes) - await page.waitForFunction(() => { - const container = document.querySelector("[data-cloud-count]"); - return container && Number(container.getAttribute("data-cloud-count")) > 0; - }, { timeout: 10_000 }); - - const count = await page.getAttribute("[data-cloud-count]", "data-cloud-count"); - expect(Number(count)).toBeGreaterThan(0); - }); - - test("project name is displayed", async ({ page }) => { - // fixture project has name "e2e-fixture-project" - await expect(page.locator("text=e2e-fixture-project")).toBeVisible(); - }); - - test("stats show file count from fixture project", async ({ page }) => { - const projectBar = page.locator("h1", { hasText: "e2e-fixture-project" }).locator(".."); - await expect(projectBar).toBeVisible(); - await expect(projectBar.getByText("Files", { exact: false }).first()).toBeVisible(); - await expect(projectBar.getByText("Dependencies")).toBeVisible(); - }); -}); diff --git a/e2e/mcp.spec.ts b/e2e/mcp.spec.ts deleted file mode 100644 index 0c8b2e9..0000000 --- a/e2e/mcp.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("MCP API", () => { - test("GET /api/mcp lists all 7 tools", async ({ request }) => { - const res = await request.get("/api/mcp"); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(Array.isArray(body.tools)).toBe(true); - expect(body.tools.length).toBe(7); - - const names = body.tools.map((t: { name: string }) => t.name); - expect(names).toContain("codebase_overview"); - expect(names).toContain("file_context"); - expect(names).toContain("get_dependents"); - expect(names).toContain("find_hotspots"); - expect(names).toContain("get_module_structure"); - expect(names).toContain("analyze_forces"); - expect(names).toContain("find_dead_exports"); - }); - - test("POST codebase_overview returns structure", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "codebase_overview" }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - expect(body.content).toBeDefined(); - expect(body.content[0].type).toBe("text"); - - const data = JSON.parse(body.content[0].text); - expect(data.totalFiles).toBe(10); - expect(data.totalFunctions).toBeGreaterThan(0); - expect(Array.isArray(data.modules)).toBe(true); - expect(data.metrics.circularDeps).toBeGreaterThan(0); - }); - - test("POST file_context returns file details", async ({ request }) => { - // Get a valid file path first - const graphRes = await request.get("/api/graph"); - const graph = await graphRes.json(); - const filePath = graph.nodes.find((n: { type: string }) => n.type === "file").id; - - const res = await request.post("/api/mcp", { - data: { tool: "file_context", params: { filePath } }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(data.path).toBe(filePath); - expect(data.metrics).toBeDefined(); - expect(typeof data.metrics.pageRank).toBe("number"); - }); - - test("POST file_context with invalid path returns error", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "file_context", params: { filePath: "nonexistent.ts" } }, - }); - expect(res.status()).toBe(404); - const body = await res.json(); - expect(body.isError).toBe(true); - }); - - test("POST get_dependents returns blast radius", async ({ request }) => { - const graphRes = await request.get("/api/graph"); - const graph = await graphRes.json(); - const filePath = graph.nodes.find((n: { type: string }) => n.type === "file").id; - - const res = await request.post("/api/mcp", { - data: { tool: "get_dependents", params: { filePath } }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(data.file).toBe(filePath); - expect(Array.isArray(data.directDependents)).toBe(true); - expect(typeof data.totalAffected).toBe("number"); - expect(["LOW", "MEDIUM", "HIGH"]).toContain(data.riskLevel); - }); - - test("POST find_hotspots by coupling", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "find_hotspots", params: { metric: "coupling", limit: 5 } }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(data.metric).toBe("coupling"); - expect(Array.isArray(data.hotspots)).toBe(true); - expect(data.hotspots.length).toBeLessThanOrEqual(5); - expect(typeof data.summary).toBe("string"); - }); - - test("POST find_hotspots by complexity", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "find_hotspots", params: { metric: "complexity" } }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(data.metric).toBe("complexity"); - expect(Array.isArray(data.hotspots)).toBe(true); - }); - - test("POST find_hotspots by coverage", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "find_hotspots", params: { metric: "coverage" } }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(data.metric).toBe("coverage"); - }); - - test("POST get_module_structure returns modules + cross-deps", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "get_module_structure" }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(Array.isArray(data.modules)).toBe(true); - expect(data.modules.length).toBeGreaterThan(0); - expect(Array.isArray(data.crossModuleDeps)).toBe(true); - expect(Array.isArray(data.circularDeps)).toBe(true); - }); - - test("POST analyze_forces returns force analysis", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "analyze_forces" }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(Array.isArray(data.moduleCohesion)).toBe(true); - expect(Array.isArray(data.tensionFiles)).toBe(true); - expect(Array.isArray(data.bridgeFiles)).toBe(true); - expect(typeof data.summary).toBe("string"); - }); - - test("POST find_dead_exports returns unused exports", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "find_dead_exports" }, - }); - expect(res.status()).toBe(200); - const body = await res.json(); - const data = JSON.parse(body.content[0].text); - expect(typeof data.totalDeadExports).toBe("number"); - expect(Array.isArray(data.files)).toBe(true); - // Fixture project has dead exports - expect(data.totalDeadExports).toBeGreaterThan(0); - }); - - test("POST with invalid tool name returns error", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { tool: "nonexistent_tool" }, - }); - expect(res.status()).toBe(404); - const body = await res.json(); - expect(body.isError).toBe(true); - }); - - test("POST with missing tool field returns 400", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: { params: {} }, - }); - expect(res.status()).toBe(400); - }); - - test("POST with invalid JSON returns 400", async ({ request }) => { - const res = await request.post("/api/mcp", { - data: "not json", - headers: { "Content-Type": "text/plain" }, - }); - expect(res.status()).toBe(400); - }); -}); diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts deleted file mode 100644 index 5071875..0000000 --- a/e2e/navigation.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Navigation + Views", () => { - test("/ loads with canvas and all UI chrome", async ({ page }) => { - await page.goto("/"); - await page.waitForSelector("canvas", { timeout: 15_000 }); - - // ProjectBar - await expect(page.getByText("Files", { exact: false }).first()).toBeVisible(); - await expect(page.getByText("Functions")).toBeVisible(); - await expect(page.getByText("Dependencies")).toBeVisible(); - - // All 8 view tabs (buttons, not links) - const views = ["Galaxy", "Dep Flow", "Hotspot", "Focus", "Module", "Forces", "Churn", "Coverage"]; - for (const view of views) { - await expect(page.getByRole("button", { name: view })).toBeVisible(); - } - - // Search input - await expect(page.locator("input[placeholder='Search files...']")).toBeVisible(); - - // Settings panel - await expect(page.getByText("SETTINGS")).toBeVisible(); - await expect(page.getByText("NODES")).toBeVisible(); - - // Legend - await expect(page.getByText("Node color")).toBeVisible(); - - // Canvas (3D graph) - await expect(page.locator("canvas")).toBeVisible(); - }); - - test("view tab click switches view without page reload", async ({ page }) => { - await page.goto("/"); - await page.waitForSelector("canvas", { timeout: 15_000 }); - - // Click Hotspot tab - await page.getByRole("button", { name: "Hotspot" }).click(); - // URL stays the same (single page) - await expect(page).toHaveURL("/"); - // Canvas still visible - await expect(page.locator("canvas")).toBeVisible(); - - // Click Forces tab - await page.getByRole("button", { name: "Forces" }).click(); - await expect(page.locator("canvas")).toBeVisible(); - }); - -}); diff --git a/e2e/server.mjs b/e2e/server.mjs deleted file mode 100644 index 1ece989..0000000 --- a/e2e/server.mjs +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Standalone server for e2e tests. - * Bypasses CLI (commander + crash handlers) for stability. - * Usage: node e2e/server.mjs [port] - */ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { createServer } from "http"; -import { parseCodebase } from "../dist/parser/index.js"; -import { buildGraph } from "../dist/graph/index.js"; -import { analyzeGraph } from "../dist/analyzer/index.js"; -import { setGraph } from "../dist/server/graph-store.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const port = parseInt(process.argv[2] || "3344", 10); -const fixturePath = path.resolve(__dirname, "fixture-project"); - -// Parse and build graph -const files = parseCodebase(fixturePath); -const built = buildGraph(files); -const codebaseGraph = analyzeGraph(built, files); - -// Read project name from fixture package.json -let projectName = "fixture-project"; -try { - const pkg = JSON.parse(fs.readFileSync(path.resolve(fixturePath, "package.json"), "utf-8")); - if (pkg.name) projectName = pkg.name; -} catch { /* use default */ } - -setGraph(codebaseGraph, projectName); - -// Catch all errors — NEVER let the server crash during tests -process.on("uncaughtException", (err) => { - console.error("[e2e-server] uncaughtException:", err.message); -}); -process.on("unhandledRejection", (err) => { - console.error("[e2e-server] unhandledRejection:", err instanceof Error ? err.message : err); -}); - -// Start Next.js production server -const projectDir = path.resolve(__dirname, ".."); -const next = (await import("next")).default; -const app = next({ dev: false, dir: projectDir }); -const handle = app.getRequestHandler(); - -await app.prepare(); - -const server = createServer((req, res) => { - handle(req, res).catch((err) => { - console.error("[e2e-server] request error:", err.message); - if (!res.headersSent) { - res.statusCode = 500; - res.end("Internal Server Error"); - } - }); -}); - -server.on("error", (err) => { - console.error("[e2e-server] server error:", err.message); -}); - -server.listen(port, () => { - console.log(`E2E server ready at http://localhost:${port}`); -}); - -process.on("SIGINT", () => { - server.close(); - process.exit(0); -}); - -process.on("SIGTERM", () => { - server.close(); - process.exit(0); -}); diff --git a/eslint.config.js b/eslint.config.js index 653990a..30234a1 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -56,7 +56,7 @@ export default tseslint.config( }, // Test files: relax unsafe rules { - files: ["src/**/*.test.ts"], + files: ["src/**/*.test.ts", "tests/**/*.test.ts"], rules: { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-unsafe-member-access": "off", @@ -64,27 +64,7 @@ export default tseslint.config( "@typescript-eslint/no-unsafe-argument": "off", }, }, - // React/Next.js client components: relax some rules { - files: ["app/**/*.tsx", "components/**/*.tsx", "hooks/**/*.ts"], - rules: { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-return": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-require-imports": "off", - }, - }, - // Next.js API routes - { - files: ["app/api/**/*.ts"], - rules: { - "@typescript-eslint/explicit-function-return-type": "off", - }, - }, - { - ignores: ["dist/**", "public/**", ".next/**", "*.config.*", "vitest.config.ts", "postcss.config.mjs"], + ignores: ["dist/**", "*.config.*", "vitest.config.ts"], } ); diff --git a/hooks/use-graph-config.ts b/hooks/use-graph-config.ts deleted file mode 100644 index cb8cc21..0000000 --- a/hooks/use-graph-config.ts +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import { useReducer, useCallback } from "react"; -import { DEFAULT_CONFIG, type GraphConfig } from "@/lib/types"; - -type ConfigAction = - | { type: "set"; key: keyof GraphConfig; value: number | string | boolean } - | { type: "reset" }; - -function configReducer(state: GraphConfig, action: ConfigAction): GraphConfig { - switch (action.type) { - case "set": - return { ...state, [action.key]: action.value }; - case "reset": - return DEFAULT_CONFIG; - } -} - -export function useGraphConfig(): { - config: GraphConfig; - setConfig: (key: keyof GraphConfig, value: number | string | boolean) => void; - resetConfig: () => void; -} { - const [config, dispatch] = useReducer(configReducer, DEFAULT_CONFIG); - - const setConfig = useCallback( - (key: keyof GraphConfig, value: number | string | boolean) => { - dispatch({ type: "set", key, value }); - }, - [], - ); - - const resetConfig = useCallback(() => { - dispatch({ type: "reset" }); - }, []); - - return { config, setConfig, resetConfig }; -} diff --git a/hooks/use-graph-data.ts b/hooks/use-graph-data.ts deleted file mode 100644 index 0f71e6d..0000000 --- a/hooks/use-graph-data.ts +++ /dev/null @@ -1,55 +0,0 @@ -import useSWR from "swr"; -import type { GraphApiResponse, ForceApiResponse, GroupMetrics } from "@/lib/types"; - -async function fetcher(url: string): Promise { - const res = await fetch(url); - if (!res.ok) throw new Error(`Fetch failed: ${res.statusText}`); - return res.json() as Promise; -} - -interface StalenessInfo { - stale: boolean; - indexedHash: string; -} - -export function useGraphData(): { - graphData: GraphApiResponse | undefined; - forceData: ForceApiResponse | undefined; - groupData: GroupMetrics[] | undefined; - projectName: string; - staleness: StalenessInfo | undefined; - isLoading: boolean; - error: Error | undefined; -} { - const { data: graphData, error: graphError, isLoading: graphLoading } = useSWR( - "/api/graph", - fetcher, - { revalidateOnFocus: false, revalidateOnReconnect: false }, - ); - const { data: forceData, error: forceError, isLoading: forceLoading } = useSWR( - "/api/forces", - fetcher, - { revalidateOnFocus: false, revalidateOnReconnect: false }, - ); - const { data: groupResponse } = useSWR<{ groups: GroupMetrics[] }>( - "/api/groups", - fetcher, - { revalidateOnFocus: false, revalidateOnReconnect: false }, - ); - const groupData = groupResponse?.groups; - const { data: metaData } = useSWR<{ projectName: string; staleness?: StalenessInfo }>( - "/api/meta", - fetcher, - { revalidateOnFocus: false, revalidateOnReconnect: false }, - ); - - return { - graphData, - forceData, - groupData, - projectName: metaData?.projectName ?? "Codebase Intelligence", - staleness: metaData?.staleness, - isLoading: graphLoading || forceLoading, - error: graphError ?? forceError, - }; -} diff --git a/hooks/use-symbol-data.ts b/hooks/use-symbol-data.ts deleted file mode 100644 index 2f44bcb..0000000 --- a/hooks/use-symbol-data.ts +++ /dev/null @@ -1,21 +0,0 @@ -import useSWR from "swr"; -import type { SymbolGraphResponse } from "@/lib/types"; - -async function fetcher(url: string): Promise { - const res = await fetch(url); - if (!res.ok) throw new Error(`Fetch failed: ${res.statusText}`); - return res.json() as Promise; -} - -export function useSymbolData(enabled: boolean): { - symbolData: SymbolGraphResponse | undefined; - isLoading: boolean; - error: Error | undefined; -} { - const { data, error, isLoading } = useSWR( - enabled ? "/api/symbol-graph" : null, - fetcher, - { revalidateOnFocus: false, revalidateOnReconnect: false }, - ); - return { symbolData: data, isLoading, error }; -} diff --git a/lib/cluster-force.ts b/lib/cluster-force.ts deleted file mode 100644 index 4b3e8da..0000000 --- a/lib/cluster-force.ts +++ /dev/null @@ -1,60 +0,0 @@ -export interface ClusterForce { - (alpha: number): void; - initialize: (nodes: Array>) => void; - strength: (s: number) => ClusterForce; -} - -export function createClusterForce( - getClusterId: (node: Record) => string | undefined, - strength: number, -): ClusterForce { - let nodes: Array> = []; - let currentStrength = strength; - - function force(alpha: number): void { - if (currentStrength === 0) return; - const centroids = new Map(); - for (const node of nodes) { - if (node.x === undefined) continue; - const id = getClusterId(node); - if (!id) continue; - const c = centroids.get(id) ?? { x: 0, y: 0, z: 0, count: 0 }; - c.x += node.x as number; - c.y += node.y as number; - c.z += node.z as number; - c.count++; - centroids.set(id, c); - } - for (const c of centroids.values()) { - c.x /= c.count; - c.y /= c.count; - c.z /= c.count; - } - const k = currentStrength * alpha; - for (const node of nodes) { - if (node.x === undefined) continue; - const id = getClusterId(node); - if (!id) continue; - const c = centroids.get(id); - if (!c || c.count < 2) continue; - const dx = c.x - (node.x as number); - const dy = c.y - (node.y as number); - const dz = c.z - (node.z as number); - if (dx * dx + dy * dy + dz * dz < 25) continue; - node.vx = ((node.vx as number) || 0) + dx * k; - node.vy = ((node.vy as number) || 0) + dy * k; - node.vz = ((node.vz as number) || 0) + dz * k; - } - } - - force.initialize = function (n: Array>): void { - nodes = n; - }; - - force.strength = function (s: number): ClusterForce { - currentStrength = s; - return force; - }; - - return force; -} diff --git a/lib/types.ts b/lib/types.ts deleted file mode 100644 index d3cf45a..0000000 --- a/lib/types.ts +++ /dev/null @@ -1,185 +0,0 @@ -export interface GraphApiNode { - id: string; - type: "file" | "function"; - label: string; - path: string; - loc: number; - module: string; - pageRank: number; - betweenness: number; - coupling: number; - fanIn: number; - fanOut: number; - tension: number; - isBridge: boolean; - churn: number; - cyclomaticComplexity: number; - blastRadius: number; - deadExports: string[]; - hasTests: boolean; - testFile: string; - functions: Array<{ - name: string; - loc: number; - fanIn?: number; - fanOut?: number; - pageRank?: number; - }>; -} - -export interface GraphApiEdge { - source: string; - target: string; - symbols: string[]; - isTypeOnly: boolean; - weight: number; -} - -export interface GraphApiStats { - totalFiles: number; - totalFunctions: number; - totalDependencies: number; - circularDeps: string[][]; -} - -export interface GraphApiResponse { - nodes: GraphApiNode[]; - edges: GraphApiEdge[]; - stats: GraphApiStats; -} - -export interface ForceApiResponse { - moduleCohesion: Array<{ - path: string; - verdict: "COHESIVE" | "MODERATE" | "JUNK_DRAWER"; - }>; - tensionFiles: Array<{ file: string; tension: number }>; - bridgeFiles: Array<{ file: string; betweenness: number; connects: string[] }>; - extractionCandidates: Array<{ target: string; escapeVelocity: number }>; - summary: string; -} - -export interface GroupMetrics { - name: string; - files: number; - loc: number; - importance: number; - fanIn: number; - fanOut: number; - color: string; -} - -export type ViewType = - | "galaxy" - | "depflow" - | "hotspot" - | "focus" - | "module" - | "forces" - | "churn" - | "coverage" - | "symbols" - | "types"; - -export interface SymbolApiNode { - id: string; - name: string; - type: "function" | "class" | "variable" | "type" | "interface" | "enum"; - file: string; - loc: number; - isDefault: boolean; - fanIn: number; - fanOut: number; - pageRank: number; - betweenness: number; -} - -export interface CallApiEdge { - source: string; - target: string; - callerSymbol: string; - calleeSymbol: string; - confidence: "type-resolved" | "text-inferred"; -} - -export interface SymbolGraphResponse { - symbolNodes: SymbolApiNode[]; - callEdges: CallApiEdge[]; - symbolMetrics: Array<{ - symbolId: string; - name: string; - file: string; - fanIn: number; - fanOut: number; - pageRank: number; - betweenness: number; - }>; -} - -export interface GraphConfig { - nodeOpacity: number; - nodeSize: number; - isolatedDim: number; - linkColor: string; - linkOpacity: number; - linkWidth: number; - charge: number; - distance: number; - showModuleBoxes: boolean; - boxOpacity: number; - clusterStrength: number; -} - -export const DEFAULT_CONFIG: GraphConfig = { - nodeOpacity: 0.9, - nodeSize: 1.0, - isolatedDim: 0.3, - linkColor: "#969696", - linkOpacity: 0.8, - linkWidth: 0.3, - charge: -30, - distance: 120, - showModuleBoxes: true, - boxOpacity: 0.4, - clusterStrength: 0.3, -}; - -export interface RenderNode { - id: string; - path: string; - label: string; - module: string; - loc: number; - pageRank: number; - betweenness: number; - coupling: number; - fanIn: number; - fanOut: number; - tension: number; - isBridge: boolean; - churn: number; - cyclomaticComplexity: number; - blastRadius: number; - deadExports: string[]; - hasTests: boolean; - testFile: string; - functions: Array<{ - name: string; - loc: number; - fanIn?: number; - fanOut?: number; - pageRank?: number; - }>; - color: string; - size: number; - x?: number; - y?: number; - z?: number; -} - -export interface RenderLink { - source: string; - target: string; - color: string; - width: number; -} diff --git a/lib/utils.ts b/lib/utils.ts deleted file mode 100644 index a70ebb6..0000000 --- a/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]): string { - return twMerge(clsx(inputs)); -} diff --git a/lib/views.ts b/lib/views.ts deleted file mode 100644 index d9938c0..0000000 --- a/lib/views.ts +++ /dev/null @@ -1,420 +0,0 @@ -import type { - GraphApiNode, - GraphApiEdge, - GraphConfig, - ForceApiResponse, - RenderNode, - RenderLink, - SymbolApiNode, - CallApiEdge, -} from "./types"; - -const MODULE_COLORS = [ - "#2563eb", "#dc2626", "#16a34a", "#9333ea", "#ea580c", - "#0891b2", "#ca8a04", "#e11d48", "#4f46e5", "#059669", -]; - -const moduleColorMap = new Map(); -let colorIdx = 0; - -export function getModuleColor(mod: string): string { - if (!moduleColorMap.has(mod)) { - moduleColorMap.set(mod, MODULE_COLORS[colorIdx % MODULE_COLORS.length]); - colorIdx++; - } - return moduleColorMap.get(mod) ?? MODULE_COLORS[0]; -} - -function isIsolated(n: GraphApiNode): boolean { - return n.fanIn === 0 && n.fanOut === 0; -} - -function dimColor(hex: string, factor: number): string { - if (hex.startsWith("#")) { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return `rgb(${Math.round(r * factor)},${Math.round(g * factor)},${Math.round(b * factor)})`; - } - if (hex.startsWith("rgb")) { - const m = hex.match(/(\d+)/g); - if (!m) return hex; - return `rgb(${Math.round(+m[0] * factor)},${Math.round(+m[1] * factor)},${Math.round(+m[2] * factor)})`; - } - return hex; -} - -function nodeColor(n: GraphApiNode, base: string, cfg: GraphConfig): string { - return isIsolated(n) ? dimColor(base, cfg.isolatedDim) : base; -} - -function nodeSize(n: GraphApiNode, base: number, cfg: GraphConfig): number { - return (isIsolated(n) ? base * cfg.isolatedDim : base) * cfg.nodeSize; -} - -export function linkRgba(cfg: GraphConfig, alpha?: number): string { - const h = cfg.linkColor; - const r = parseInt(h.slice(1, 3), 16); - const g = parseInt(h.slice(3, 5), 16); - const b = parseInt(h.slice(5, 7), 16); - return `rgba(${r},${g},${b},${alpha ?? cfg.linkOpacity})`; -} - -export function healthColor(score: number): string { - const r = Math.min(255, Math.floor(score * 2 * 255)); - const g = Math.min(255, Math.floor((1 - score) * 2 * 255)); - return `rgb(${r},${g},60)`; -} - -export function complexityLabel(val: number): { text: string; color: string } { - if (val <= 5) return { text: "Simple", color: "#16a34a" }; - if (val <= 10) return { text: "Moderate", color: "#ca8a04" }; - if (val <= 20) return { text: "Complex", color: "#ea580c" }; - return { text: "Very Complex", color: "#dc2626" }; -} - -function toRenderNode(n: GraphApiNode, color: string, size: number): RenderNode { - return { ...n, color, size }; -} - -function toRenderLink(source: string, target: string, color: string, width: number): RenderLink { - return { source, target, color, width }; -} - -export function galaxyView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - return { - nodes: nodes.map((n) => { - const base = getModuleColor(n.module); - return toRenderNode(n, nodeColor(n, base, cfg), nodeSize(n, 2 + Math.sqrt(n.pageRank * 10000), cfg)); - }), - links: edges.map((e) => - toRenderLink(e.source, e.target, e.isTypeOnly ? linkRgba(cfg, 0.4) : linkRgba(cfg, 0.6), cfg.linkWidth), - ), - }; -} - -export function depFlowView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, - circularDeps: string[][], -): { nodes: RenderNode[]; links: RenderLink[] } { - const circularPairs = new Set(); - circularDeps.forEach((cycle) => { - for (let i = 0; i < cycle.length - 1; i++) { - circularPairs.add(`${cycle[i]}->${cycle[i + 1]}`); - } - }); - - return { - nodes: nodes.map((n) => { - const base = getModuleColor(n.module); - return toRenderNode(n, nodeColor(n, base, cfg), nodeSize(n, 2 + Math.sqrt(n.pageRank * 10000), cfg)); - }), - links: edges.map((e) => { - const isCircular = - circularPairs.has(`${e.source}->${e.target}`) || circularPairs.has(`${e.target}->${e.source}`); - return toRenderLink( - e.source, - e.target, - isCircular ? "rgba(220,38,38,0.8)" : linkRgba(cfg, 0.6), - isCircular ? 2 : cfg.linkWidth, - ); - }), - }; -} - -export function hotspotView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - return { - nodes: nodes.map((n) => { - const base = healthColor(n.coupling); - return toRenderNode(n, nodeColor(n, base, cfg), nodeSize(n, 1 + n.loc / 20, cfg)); - }), - links: edges.map((e) => toRenderLink(e.source, e.target, linkRgba(cfg, 0.5), cfg.linkWidth)), - }; -} - -export function focusView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, - targetId: string, -): { nodes: RenderNode[]; links: RenderLink[] } { - const neighbors = new Set([targetId]); - edges.forEach((e) => { - if (e.source === targetId) neighbors.add(e.target); - if (e.target === targetId) neighbors.add(e.source); - }); - const hop2 = new Set(neighbors); - edges.forEach((e) => { - if (neighbors.has(e.source)) hop2.add(e.target); - if (neighbors.has(e.target)) hop2.add(e.source); - }); - - return { - nodes: nodes.map((n) => { - const inFocus = hop2.has(n.id); - const color = - n.id === targetId - ? "#fbbf24" - : neighbors.has(n.id) - ? getModuleColor(n.module) - : inFocus - ? getModuleColor(n.module) - : "#1a1a24"; - const size = - (n.id === targetId ? 8 : neighbors.has(n.id) ? 4 : inFocus ? 2 : 0.5) * cfg.nodeSize; - return toRenderNode(n, color, size); - }), - links: edges.map((e) => - toRenderLink( - e.source, - e.target, - hop2.has(e.source) && hop2.has(e.target) ? linkRgba(cfg, 0.7) : linkRgba(cfg, 0.1), - cfg.linkWidth, - ), - ), - }; -} - -export function moduleView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, - nodeById: Map, -): { nodes: RenderNode[]; links: RenderLink[] } { - return { - nodes: nodes.map((n) => { - const base = getModuleColor(n.module); - return toRenderNode(n, nodeColor(n, base, cfg), nodeSize(n, 2 + Math.sqrt(n.pageRank * 10000), cfg)); - }), - links: edges.map((e) => { - const sn = nodeById.get(e.source); - const tn = nodeById.get(e.target); - const crossModule = sn !== undefined && tn !== undefined && sn.module !== tn.module; - return toRenderLink( - e.source, - e.target, - crossModule ? "rgba(255,200,50,0.5)" : linkRgba(cfg, 0.4), - crossModule ? 1.5 : cfg.linkWidth, - ); - }), - }; -} - -export function forcesView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, - forceData: ForceApiResponse, - nodeById: Map, -): { nodes: RenderNode[]; links: RenderLink[] } { - const tensionSet = new Set(forceData.tensionFiles.map((t) => t.file)); - const bridgeSet = new Set(forceData.bridgeFiles.map((b) => b.file)); - const junkModules = new Set(forceData.moduleCohesion.filter((m) => m.verdict === "JUNK_DRAWER").map((m) => m.path)); - const extractModules = new Set(forceData.extractionCandidates.map((e) => e.target)); - - return { - nodes: nodes.map((n) => { - let color = getModuleColor(n.module); - let size = 2 + Math.sqrt(n.pageRank * 10000); - if (tensionSet.has(n.id)) { color = "#fbbf24"; size *= 1.5; } - else if (bridgeSet.has(n.id)) { color = "#06b6d4"; size *= 1.3; } - else if (junkModules.has(n.module)) { color = "#ef4444"; } - else if (extractModules.has(n.module)) { color = "#22c55e"; } - return toRenderNode(n, nodeColor(n, color, cfg), nodeSize(n, size, cfg)); - }), - links: edges.map((e) => { - const sn = nodeById.get(e.source); - const tn = nodeById.get(e.target); - const crossModule = sn !== undefined && tn !== undefined && sn.module !== tn.module; - return toRenderLink( - e.source, - e.target, - crossModule ? "rgba(239,68,68,0.4)" : linkRgba(cfg, 0.4), - crossModule ? 1 : cfg.linkWidth, - ); - }), - }; -} - -export function churnView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - const maxChurn = Math.max(1, ...nodes.map((n) => n.churn)); - - return { - nodes: nodes.map((n) => { - const score = n.churn / maxChurn; - return toRenderNode(n, nodeColor(n, healthColor(score), cfg), nodeSize(n, 2 + (n.churn / maxChurn) * 6, cfg)); - }), - links: edges.map((e) => toRenderLink(e.source, e.target, linkRgba(cfg, 0.4), cfg.linkWidth)), - }; -} - -export function coverageView( - nodes: GraphApiNode[], - edges: GraphApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - return { - nodes: nodes.map((n) => { - const base = n.hasTests ? "#16a34a" : "#dc2626"; - return toRenderNode(n, nodeColor(n, base, cfg), nodeSize(n, 2 + Math.sqrt(n.pageRank * 10000), cfg)); - }), - links: edges.map((e) => toRenderLink(e.source, e.target, linkRgba(cfg, 0.4), cfg.linkWidth)), - }; -} - -const SYMBOL_TYPE_COLORS: Record = { - function: "#2563eb", - class: "#16a34a", - interface: "#9333ea", - type: "#ea580c", - enum: "#ca8a04", - variable: "#6b7280", -}; - -function symbolTypeColor(type: string): string { - return SYMBOL_TYPE_COLORS[type] ?? "#6b7280"; -} - -function symbolModule(filePath: string): string { - const parts = filePath.split("/"); - if (parts.length <= 1) return "."; - return parts.slice(0, -1).join("/") + "/"; -} - -function toSymbolRenderNode( - s: SymbolApiNode, - color: string, - size: number, -): RenderNode { - return { - id: s.id, - path: s.file, - label: s.name, - module: symbolModule(s.file), - loc: s.loc, - pageRank: s.pageRank, - betweenness: s.betweenness, - coupling: 0, - fanIn: s.fanIn, - fanOut: s.fanOut, - tension: 0, - isBridge: false, - churn: 0, - cyclomaticComplexity: 0, - blastRadius: 0, - deadExports: [], - hasTests: false, - testFile: "", - functions: [], - color, - size, - }; -} - -export function symbolView( - symbolNodes: SymbolApiNode[], - callEdges: CallApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - const nodeIds = new Set(symbolNodes.map((s) => s.id)); - const validEdges = callEdges.filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target)); - return { - nodes: symbolNodes.map((s) => { - const color = symbolTypeColor(s.type); - const size = (2 + Math.sqrt(s.pageRank * 10000)) * cfg.nodeSize; - return toSymbolRenderNode(s, color, size); - }), - links: validEdges.map((e) => - toRenderLink( - e.source, - e.target, - e.confidence === "type-resolved" ? linkRgba(cfg, 0.6) : linkRgba(cfg, 0.3), - cfg.linkWidth, - ), - ), - }; -} - -export function typesView( - symbolNodes: SymbolApiNode[], - callEdges: CallApiEdge[], - cfg: GraphConfig, -): { nodes: RenderNode[]; links: RenderLink[] } { - const typeKinds = new Set(["interface", "type", "enum"]); - const typeNodes = symbolNodes.filter((s) => typeKinds.has(s.type)); - const typeIds = new Set(typeNodes.map((s) => s.id)); - const typeEdges = callEdges.filter((e) => typeIds.has(e.source) && typeIds.has(e.target)); - - return { - nodes: typeNodes.map((s) => { - const color = symbolTypeColor(s.type); - const size = (2 + Math.sqrt(s.pageRank * 10000)) * cfg.nodeSize; - return toSymbolRenderNode(s, color, size); - }), - links: typeEdges.map((e) => - toRenderLink(e.source, e.target, linkRgba(cfg, 0.5), cfg.linkWidth), - ), - }; -} - -export const LEGENDS: Record> = { - galaxy: [{ color: "", label: "Node color = module | Size = importance (PageRank)" }], - depflow: [ - { color: "", label: "Top = entry points | Bottom = leaf deps" }, - { color: "#dc2626", label: "Red edges = circular deps" }, - ], - hotspot: [ - { color: "#16a34a", label: "Green = healthy" }, - { color: "#ea580c", label: "Orange = moderate" }, - { color: "#dc2626", label: "Red = high coupling" }, - ], - focus: [ - { color: "#fbbf24", label: "Yellow = selected" }, - { color: "", label: "Bright = neighbors | Faded = distant" }, - ], - module: [ - { color: "", label: "Color = module" }, - { color: "#fbbf24", label: "Yellow edges = cross-module deps" }, - ], - forces: [ - { color: "#fbbf24", label: "Tension" }, - { color: "#06b6d4", label: "Bridge" }, - { color: "#ef4444", label: "Junk drawer" }, - { color: "#22c55e", label: "Extraction candidate" }, - ], - churn: [ - { color: "#16a34a", label: "Green = stable" }, - { color: "#ea580c", label: "Orange = moderate" }, - { color: "#dc2626", label: "Red = high churn" }, - ], - coverage: [ - { color: "#16a34a", label: "Green = has tests" }, - { color: "#dc2626", label: "Red = untested" }, - ], - symbols: [ - { color: "#2563eb", label: "Blue = function" }, - { color: "#16a34a", label: "Green = class" }, - { color: "#9333ea", label: "Purple = interface" }, - { color: "#ea580c", label: "Orange = type" }, - { color: "#ca8a04", label: "Yellow = enum" }, - ], - types: [ - { color: "#9333ea", label: "Purple = interface" }, - { color: "#ea580c", label: "Orange = type" }, - { color: "#ca8a04", label: "Yellow = enum" }, - ], -}; diff --git a/next.config.ts b/next.config.ts deleted file mode 100644 index b6d5926..0000000 --- a/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - serverExternalPackages: ["graphology", "graphology-metrics", "graphology-shortest-path", "typescript"], -}; - -export default nextConfig; diff --git a/package.json b/package.json index 6472a85..85b3ad8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codebase-intelligence", "version": "1.1.0", - "description": "3D interactive codebase visualization with MCP integration for LLM-assisted code understanding", + "description": "Codebase analysis engine with MCP integration for LLM-assisted code understanding", "type": "module", "main": "dist/cli.js", "bin": { @@ -9,30 +9,25 @@ }, "packageManager": "pnpm@10.28.2", "scripts": { - "build": "tsc -p tsconfig.build.json && next build", - "build:cli": "tsc -p tsconfig.build.json", + "build": "tsc -p tsconfig.build.json", "dev": "tsx src/cli.ts", "start": "node dist/cli.js", "test": "vitest run", "test:watch": "vitest", - "test:e2e": "playwright test", - "lint": "eslint src/ app/ components/ hooks/ lib/", + "lint": "eslint src/", "typecheck": "tsc --noEmit", - "clean": "rm -rf dist .next", - "publish:npm": "pnpm lint && pnpm typecheck && pnpm build && pnpm test && npm publish --access public --registry https://registry.npmjs.org", - "publish:github": "pnpm lint && pnpm typecheck && pnpm build && pnpm test && scripts/publish-github.sh" + "clean": "rm -rf dist", + "publish:npm": "pnpm lint && pnpm typecheck && pnpm build && pnpm test && npm publish --access public --registry https://registry.npmjs.org" }, "keywords": [ "codebase", - "visualizer", - "3d", - "code-map", + "code-analysis", "dependency-graph", "mcp", "typescript", "architecture", - "code-analysis", - "force-directed-graph" + "metrics", + "static-analysis" ], "author": "bntvllnt", "license": "MIT", @@ -45,38 +40,20 @@ }, "homepage": "https://github.com/bntvllnt/codebase-intelligence#readme", "dependencies": { - "3d-force-graph": "^1.79.1", "@modelcontextprotocol/sdk": "^1.0.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", "commander": "^12.0.0", "graphology": "^0.25.4", "graphology-communities-louvain": "^2.0.2", "graphology-metrics": "^2.3.0", "graphology-shortest-path": "^2.1.0", - "lucide-react": "^0.574.0", - "next": "^16.1.6", - "open": "^11.0.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-force-graph-3d": "^1.29.1", - "swr": "^2.4.0", - "tailwind-merge": "^3.4.1", - "three": "^0.183.0", "typescript": "^5.7.0", "zod": "^3.23.0" }, "devDependencies": { "@eslint/js": "^10.0.1", - "@playwright/test": "^1.58.2", - "@tailwindcss/postcss": "^4.2.0", "@types/node": "^22.0.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/three": "^0.182.0", + "@vitest/coverage-v8": "^3.2.4", "eslint": "^10.0.0", - "playwright": "^1.58.2", - "tailwindcss": "^4.2.0", "tsx": "^4.21.0", "typescript-eslint": "^8.56.0", "vitest": "^3.0.0" @@ -85,11 +62,6 @@ "node": ">=18" }, "files": [ - "dist", - "app", - "components", - "hooks", - "lib", - "public" + "dist" ] } diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index e3bd91d..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from "@playwright/test"; - -export default defineConfig({ - testDir: "./e2e", - timeout: 30_000, - retries: 1, - workers: 1, - use: { - baseURL: "http://localhost:3344", - headless: true, - }, - webServer: { - command: "NODE_ENV=production node e2e/server.mjs 3344", - port: 3344, - timeout: 60_000, - reuseExistingServer: !process.env.CI, - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 989a18b..7f36c6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,18 +8,9 @@ importers: .: dependencies: - 3d-force-graph: - specifier: ^1.79.1 - version: 1.79.1 '@modelcontextprotocol/sdk': specifier: ^1.0.0 version: 1.26.0(zod@3.25.76) - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.1.1 - version: 2.1.1 commander: specifier: ^12.0.0 version: 12.1.0 @@ -35,33 +26,6 @@ importers: graphology-shortest-path: specifier: ^2.1.0 version: 2.1.0(graphology-types@0.24.8) - lucide-react: - specifier: ^0.574.0 - version: 0.574.0(react@19.2.4) - next: - specifier: ^16.1.6 - version: 16.1.6(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - open: - specifier: ^11.0.0 - version: 11.0.0 - react: - specifier: ^19.2.4 - version: 19.2.4 - react-dom: - specifier: ^19.2.4 - version: 19.2.4(react@19.2.4) - react-force-graph-3d: - specifier: ^1.29.1 - version: 1.29.1(react@19.2.4) - swr: - specifier: ^2.4.0 - version: 2.4.0(react@19.2.4) - tailwind-merge: - specifier: ^3.4.1 - version: 3.4.1 - three: - specifier: ^0.183.0 - version: 0.183.0 typescript: specifier: ^5.7.0 version: 5.9.3 @@ -72,33 +36,15 @@ importers: '@eslint/js': specifier: ^10.0.1 version: 10.0.1(eslint@10.0.0(jiti@2.6.1)) - '@playwright/test': - specifier: ^1.58.2 - version: 1.58.2 - '@tailwindcss/postcss': - specifier: ^4.2.0 - version: 4.2.0 '@types/node': specifier: ^22.0.0 version: 22.19.11 - '@types/react': - specifier: ^19.2.14 - version: 19.2.14 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.14) - '@types/three': - specifier: ^0.182.0 - version: 0.182.0 + '@vitest/coverage-v8': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) eslint: specifier: ^10.0.0 version: 10.0.0(jiti@2.6.1) - playwright: - specifier: ^1.58.2 - version: 1.58.2 - tailwindcss: - specifier: ^4.2.0 - version: 4.2.0 tsx: specifier: ^4.21.0 version: 4.21.0 @@ -111,23 +57,30 @@ importers: packages: - 3d-force-graph@1.79.1: - resolution: {integrity: sha512-iscIVt4jWjJ11KEEswgOIOWk8Ew4EFKHRyERJXJ0ouycqzHCtWwb9E5imnxS5rYF1f1IESkFNAfB+h3EkU0Irw==} - engines: {node: '>=12'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@dimforge/rapier3d-compat@0.12.0': - resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@babel/parser@7.29.0': + resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} @@ -346,165 +299,17 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@img/colour@1.0.0': - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} '@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'} @@ -525,65 +330,9 @@ packages: '@cfworker/json-schema': optional: true - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} - - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} - engines: {node: '>=18'} - hasBin: true + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} '@rollup/rollup-android-arm-eabi@4.57.1': resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} @@ -723,107 +472,6 @@ packages: cpu: [x64] os: [win32] - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - - '@tailwindcss/node@4.2.0': - resolution: {integrity: sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==} - - '@tailwindcss/oxide-android-arm64@4.2.0': - resolution: {integrity: sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [android] - - '@tailwindcss/oxide-darwin-arm64@4.2.0': - resolution: {integrity: sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [darwin] - - '@tailwindcss/oxide-darwin-x64@4.2.0': - resolution: {integrity: sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==} - engines: {node: '>= 20'} - cpu: [x64] - os: [darwin] - - '@tailwindcss/oxide-freebsd-x64@4.2.0': - resolution: {integrity: sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [freebsd] - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': - resolution: {integrity: sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==} - engines: {node: '>= 20'} - cpu: [arm] - os: [linux] - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': - resolution: {integrity: sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-arm64-musl@4.2.0': - resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-linux-x64-gnu@4.2.0': - resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@tailwindcss/oxide-linux-x64-musl@4.2.0': - resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==} - engines: {node: '>= 20'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@tailwindcss/oxide-wasm32-wasi@4.2.0': - resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - bundledDependencies: - - '@napi-rs/wasm-runtime' - - '@emnapi/core' - - '@emnapi/runtime' - - '@tybys/wasm-util' - - '@emnapi/wasi-threads' - - tslib - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': - resolution: {integrity: sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==} - engines: {node: '>= 20'} - cpu: [arm64] - os: [win32] - - '@tailwindcss/oxide-win32-x64-msvc@4.2.0': - resolution: {integrity: sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==} - engines: {node: '>= 20'} - cpu: [x64] - os: [win32] - - '@tailwindcss/oxide@4.2.0': - resolution: {integrity: sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==} - engines: {node: '>= 20'} - - '@tailwindcss/postcss@4.2.0': - resolution: {integrity: sha512-u6YBacGpOm/ixPfKqfgrJEjMfrYmPD7gEFRoygS/hnQaRtV0VCBdpkx5Ouw9pnaLRwwlgGCuJw8xLpaR0hOrQg==} - - '@tweenjs/tween.js@23.1.3': - resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} - - '@tweenjs/tween.js@25.0.0': - resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -842,23 +490,6 @@ packages: '@types/node@22.19.11': resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} - '@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.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} - - '@types/stats.js@0.17.4': - resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} - - '@types/three@0.182.0': - resolution: {integrity: sha512-WByN9V3Sbwbe2OkWuSGyoqQO8Du6yhYaXtXLoA5FkKTUJorZ+yOHBZ35zUUPQXlAKABZmbYp5oAqpA4RBjtJ/Q==} - - '@types/webxr@0.5.24': - resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} - '@typescript-eslint/eslint-plugin@8.56.0': resolution: {integrity: sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -918,6 +549,15 @@ packages: resolution: {integrity: sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/coverage-v8@3.2.4': + resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} + peerDependencies: + '@vitest/browser': 3.2.4 + vitest: 3.2.4 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -947,9 +587,6 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@webgpu/types@0.1.69': - resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} - '@yomguithereal/helpers@1.1.1': resolution: {integrity: sha512-UYvAq/XCA7xoh1juWDYsq3W0WywOB+pz8cgVnE1b45ZfdMhBvHDrgmSFG3jXeZSr2tMTYLGHFHON+ekG05Jebg==} @@ -957,10 +594,6 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} - accessor-fn@1.5.3: - resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} - engines: {node: '>=12'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -985,10 +618,29 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + 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'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -996,10 +648,6 @@ packages: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -1011,10 +659,6 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} - bundle-name@4.1.0: - resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} - engines: {node: '>=18'} - bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -1031,9 +675,6 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - caniuse-lite@1.0.30001770: - resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} - chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1042,15 +683,12 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} @@ -1080,71 +718,6 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-binarytree@1.0.2: - resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-force-3d@3.0.6: - resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} - - d3-octree@1.1.0: - resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} - - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} - - 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-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'} - - data-bind-mapper@1.0.3: - resolution: {integrity: sha512-QmU3lyEnbENQPo0M1F9BMu4s6cqNNp8iJA+b/HP2sSb7pf3dxwF3+EP1eO69rwBfH9kFJ1apmzrtogAmVt2/Xw==} - engines: {node: '>=12'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -1161,26 +734,10 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - default-browser-id@5.0.1: - resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} - engines: {node: '>=18'} - - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} - engines: {node: '>=18'} - - define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - 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'} @@ -1189,17 +746,22 @@ packages: 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==} + 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'} - enhanced-resolve@5.19.0: - resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} - engines: {node: '>=10.13.0'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1323,9 +885,6 @@ packages: picomatch: optional: true - fflate@0.8.2: - resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1345,9 +904,9 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - float-tooltip@1.7.5: - resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==} - engines: {node: '>=12'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -1357,11 +916,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1385,13 +939,15 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + 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 + 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==} - graphology-communities-louvain@2.0.2: resolution: {integrity: sha512-zt+2hHVPYxjEquyecxWXoUoIuN/UvYzsvI7boDdMNz0rRvpESQ7+e+Ejv6wK7AThycbZXuQ6DkG8NPMCq6XwoA==} peerDependencies: @@ -1425,6 +981,10 @@ packages: peerDependencies: graphology-types: '>=0.24.0' + 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'} @@ -1437,6 +997,9 @@ packages: resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} engines: {node: '>=16.9.0'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -1460,10 +1023,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - ip-address@10.0.1: resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} engines: {node: '>= 12'} @@ -1472,41 +1031,42 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - 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-in-ssh@1.0.0: - resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} - engines: {node: '>=20'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jerrypick@1.1.2: - resolution: {integrity: sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==} - engines: {node: '>=12'} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} @@ -1515,8 +1075,8 @@ packages: jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1536,10 +1096,6 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - kapsule@1.16.3: - resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} - engines: {node: '>=12'} - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -1625,24 +1181,22 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - lucide-react@0.574.0: - resolution: {integrity: sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==} - peerDependencies: - react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1655,9 +1209,6 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} - meshoptimizer@0.22.0: - resolution: {integrity: sha512-IebiK79sqIy+E4EgOr+CAw+Ke8hAspXKzBd0JdgEmPHiAwmvEj2S4h1rfvo+o/BnfEYd/jAOg5IeeIjzlzSnDg==} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} @@ -1670,10 +1221,18 @@ packages: resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} engines: {node: 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@9.0.5: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mnemonist@0.39.8: resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} @@ -1692,42 +1251,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - ngraph.events@1.4.0: - resolution: {integrity: sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==} - - ngraph.forcelayout@3.3.1: - resolution: {integrity: sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==} - - ngraph.graph@20.1.2: - resolution: {integrity: sha512-W/G3GBR3Y5UxMLHTUCPP9v+pbtpzwuAEIqP5oZV+9IwgxAIEZwh+Foc60iPc1idlnK7Zxu0p3puxAyNmDvBd0Q==} - - ngraph.merge@1.0.0: - resolution: {integrity: sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==} - - ngraph.random@1.2.0: - resolution: {integrity: sha512-4EUeAGbB2HWX9njd6bP6tciN6ByJfoaAvmVL9QTaZSeXrW46eNGA9GajiXiPBbvFqxUWFkEbyo6x5qsACUuVfA==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1746,10 +1269,6 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - open@11.0.0: - resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} - engines: {node: '>=20'} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1762,6 +1281,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pandemonium@2.4.1: resolution: {integrity: sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==} @@ -1777,6 +1299,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -1798,42 +1324,14 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} - engines: {node: '>=18'} - hasBin: true - - polished@4.3.1: - resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} - engines: {node: '>=10'} - - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - powershell-utils@0.1.0: - resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} - engines: {node: '>=20'} - - preact@10.28.3: - resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -1854,30 +1352,6 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} - peerDependencies: - react: ^19.2.4 - - react-force-graph-3d@1.29.1: - resolution: {integrity: sha512-5Vp+PGpYnO+zLwgK2NvNqdXHvsWLrFzpDfJW1vUA1twjo9SPvXqfUYQrnRmAbD+K2tOxkZw1BkbH31l5b4TWHg==} - engines: {node: '>=12'} - peerDependencies: - react: '*' - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-kapsule@2.5.7: - resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==} - engines: {node: '>=12'} - peerDependencies: - react: '>=16.13.1' - - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} - 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'} @@ -1894,16 +1368,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} - run-applescript@7.1.0: - resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} - engines: {node: '>=18'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -1920,10 +1387,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1951,6 +1414,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1965,58 +1432,36 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - - swr@2.4.0: - resolution: {integrity: sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - tailwind-merge@3.4.1: - resolution: {integrity: sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} - tailwindcss@4.2.0: - resolution: {integrity: sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} - tapable@2.3.0: - resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} - engines: {node: '>=6'} + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} - three-forcegraph@1.43.1: - resolution: {integrity: sha512-lQnYPLvR31gb91mF5xHhU0jPHJgBPw9QB23R6poCk8Tgvz8sQtq7wTxwClcPdfKCBbHXsb7FSqK06Osiu1kQ5A==} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - peerDependencies: - three: '>=0.118.3' - three-render-objects@1.40.4: - resolution: {integrity: sha512-Ukpu1pei3L5r809izvjsZxwuRcYLiyn6Uvy3lZ9bpMTdvj3i6PeX6w++/hs2ZS3KnEzGjb6YvTvh4UQuwHTDJg==} - engines: {node: '>=12'} - peerDependencies: - three: '>=0.168' + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} - three@0.183.0: - resolution: {integrity: sha512-G6SH2jfefIVa2YI4JL2VbgQhrrbp1A8dRc7lr3PW827kdVyaX2RgH6M5FmjmdVFLgSHppyg3OYOZdTfWElle+g==} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -2046,9 +1491,6 @@ packages: peerDependencies: typescript: '>=4.8.4' - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -2084,11 +1526,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - 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 - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -2180,13 +1617,17 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + 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==} - wsl-utils@0.3.1: - resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} - engines: {node: '>=20'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2201,24 +1642,25 @@ packages: snapshots: - 3d-force-graph@1.79.1: + '@ampproject/remapping@2.3.0': dependencies: - accessor-fn: 1.5.3 - kapsule: 1.16.3 - three: 0.183.0 - three-forcegraph: 1.43.1(three@0.183.0) - three-render-objects: 1.40.4(three@0.183.0) + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - '@alloc/quick-lru@5.2.0': {} + '@babel/helper-string-parser@7.27.1': {} - '@babel/runtime@7.28.6': {} + '@babel/helper-validator-identifier@7.28.5': {} - '@dimforge/rapier3d-compat@0.12.0': {} + '@babel/parser@7.29.0': + dependencies: + '@babel/types': 7.29.0 - '@emnapi/runtime@1.8.1': + '@babel/types@7.29.0': dependencies: - tslib: 2.8.1 - optional: true + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@1.0.2': {} '@esbuild/aix-ppc64@0.27.3': optional: true @@ -2332,128 +1774,37 @@ snapshots: '@eslint/core': 1.1.0 levn: 0.4.1 - '@hono/node-server@1.19.9(hono@4.11.10)': - dependencies: - hono: 4.11.10 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - - '@img/colour@1.0.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true + '@hono/node-server@1.19.9(hono@4.11.10)': + dependencies: + hono: 4.11.10 - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true + '@humanfs/core@0.19.1': {} - '@img/sharp-wasm32@0.34.5': + '@humanfs/node@0.16.7': dependencies: - '@emnapi/runtime': 1.8.1 - optional: true + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 - '@img/sharp-win32-arm64@0.34.5': - optional: true + '@humanwhocodes/module-importer@1.0.1': {} - '@img/sharp-win32-ia32@0.34.5': - optional: true + '@humanwhocodes/retry@0.4.3': {} - '@img/sharp-win32-x64@0.34.5': - optional: true + '@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 + + '@istanbuljs/schema@0.1.3': {} '@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/sourcemap-codec@1.5.5': {} @@ -2485,36 +1836,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@next/env@16.1.6': {} - - '@next/swc-darwin-arm64@16.1.6': - optional: true - - '@next/swc-darwin-x64@16.1.6': - optional: true - - '@next/swc-linux-arm64-gnu@16.1.6': - optional: true - - '@next/swc-linux-arm64-musl@16.1.6': - optional: true - - '@next/swc-linux-x64-gnu@16.1.6': - optional: true - - '@next/swc-linux-x64-musl@16.1.6': + '@pkgjs/parseargs@0.11.0': optional: true - '@next/swc-win32-arm64-msvc@16.1.6': - optional: true - - '@next/swc-win32-x64-msvc@16.1.6': - optional: true - - '@playwright/test@1.58.2': - dependencies: - playwright: 1.58.2 - '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -2590,83 +1914,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true - '@swc/helpers@0.5.15': - dependencies: - tslib: 2.8.1 - - '@tailwindcss/node@4.2.0': - dependencies: - '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.19.0 - jiti: 2.6.1 - lightningcss: 1.31.1 - magic-string: 0.30.21 - source-map-js: 1.2.1 - tailwindcss: 4.2.0 - - '@tailwindcss/oxide-android-arm64@4.2.0': - optional: true - - '@tailwindcss/oxide-darwin-arm64@4.2.0': - optional: true - - '@tailwindcss/oxide-darwin-x64@4.2.0': - optional: true - - '@tailwindcss/oxide-freebsd-x64@4.2.0': - optional: true - - '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0': - optional: true - - '@tailwindcss/oxide-linux-arm64-gnu@4.2.0': - optional: true - - '@tailwindcss/oxide-linux-arm64-musl@4.2.0': - optional: true - - '@tailwindcss/oxide-linux-x64-gnu@4.2.0': - optional: true - - '@tailwindcss/oxide-linux-x64-musl@4.2.0': - optional: true - - '@tailwindcss/oxide-wasm32-wasi@4.2.0': - optional: true - - '@tailwindcss/oxide-win32-arm64-msvc@4.2.0': - optional: true - - '@tailwindcss/oxide-win32-x64-msvc@4.2.0': - optional: true - - '@tailwindcss/oxide@4.2.0': - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.2.0 - '@tailwindcss/oxide-darwin-arm64': 4.2.0 - '@tailwindcss/oxide-darwin-x64': 4.2.0 - '@tailwindcss/oxide-freebsd-x64': 4.2.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.2.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.2.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.2.0 - '@tailwindcss/oxide-linux-x64-musl': 4.2.0 - '@tailwindcss/oxide-wasm32-wasi': 4.2.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.2.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.2.0 - - '@tailwindcss/postcss@4.2.0': - dependencies: - '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.2.0 - '@tailwindcss/oxide': 4.2.0 - postcss: 8.5.6 - tailwindcss: 4.2.0 - - '@tweenjs/tween.js@23.1.3': {} - - '@tweenjs/tween.js@25.0.0': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -2684,28 +1931,6 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/react-dom@19.2.3(@types/react@19.2.14)': - dependencies: - '@types/react': 19.2.14 - - '@types/react@19.2.14': - dependencies: - csstype: 3.2.3 - - '@types/stats.js@0.17.4': {} - - '@types/three@0.182.0': - dependencies: - '@dimforge/rapier3d-compat': 0.12.0 - '@tweenjs/tween.js': 23.1.3 - '@types/stats.js': 0.17.4 - '@types/webxr': 0.5.24 - '@webgpu/types': 0.1.69 - fflate: 0.8.2 - meshoptimizer: 0.22.0 - - '@types/webxr@0.5.24': {} - '@typescript-eslint/eslint-plugin@8.56.0(@typescript-eslint/parser@8.56.0(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -2797,6 +2022,25 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.0 + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -2839,8 +2083,6 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@webgpu/types@0.1.69': {} - '@yomguithereal/helpers@1.1.1': {} accepts@2.0.0: @@ -2848,8 +2090,6 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 - accessor-fn@1.5.3: {} - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -2874,14 +2114,28 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + 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: {} + assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + balanced-match@1.0.2: {} balanced-match@4.0.3: {} - baseline-browser-mapping@2.9.19: {} - body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -2904,10 +2158,6 @@ snapshots: dependencies: balanced-match: 4.0.3 - bundle-name@4.1.0: - dependencies: - run-applescript: 7.1.0 - bytes@3.1.2: {} cac@6.7.14: {} @@ -2922,8 +2172,6 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - caniuse-lite@1.0.30001770: {} - chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -2934,13 +2182,11 @@ snapshots: check-error@2.1.3: {} - class-variance-authority@0.7.1: + color-convert@2.0.1: dependencies: - clsx: 2.1.1 + color-name: 1.1.4 - client-only@0.0.1: {} - - clsx@2.1.1: {} + color-name@1.1.4: {} commander@12.1.0: {} @@ -2963,65 +2209,6 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.2.3: {} - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-binarytree@1.0.2: {} - - d3-color@3.1.0: {} - - d3-dispatch@3.0.1: {} - - d3-force-3d@3.0.6: - dependencies: - d3-binarytree: 1.0.2 - d3-dispatch: 3.0.1 - d3-octree: 1.1.0 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-octree@1.1.0: {} - - d3-quadtree@3.0.1: {} - - 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-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: {} - - data-bind-mapper@1.0.3: - dependencies: - accessor-fn: 1.5.3 - debug@4.4.3: dependencies: ms: 2.1.3 @@ -3030,20 +2217,10 @@ snapshots: deep-is@0.1.4: {} - default-browser-id@5.0.1: {} - - default-browser@5.5.0: - dependencies: - bundle-name: 4.1.0 - default-browser-id: 5.0.1 - - define-lazy-prop@3.0.0: {} - depd@2.0.0: {} - dequal@2.0.3: {} - - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true dunder-proto@1.0.1: dependencies: @@ -3051,14 +2228,15 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} - encodeurl@2.0.0: {} + emoji-regex@8.0.0: {} - enhanced-resolve@5.19.0: - dependencies: - graceful-fs: 4.2.11 - tapable: 2.3.0 + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} es-define-property@1.0.1: {} @@ -3235,8 +2413,6 @@ snapshots: optionalDependencies: picomatch: 4.0.3 - fflate@0.8.2: {} - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -3264,19 +2440,15 @@ snapshots: flatted@3.3.3: {} - float-tooltip@1.7.5: + foreground-child@3.3.1: dependencies: - d3-selection: 3.0.0 - kapsule: 1.16.3 - preact: 10.28.3 + cross-spawn: 7.0.6 + signal-exit: 4.1.0 forwarded@0.2.0: {} fresh@2.0.0: {} - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -3308,9 +2480,16 @@ snapshots: dependencies: is-glob: 4.0.3 - gopd@1.2.0: {} + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 - graceful-fs@4.2.11: {} + gopd@1.2.0: {} graphology-communities-louvain@2.0.2(graphology-types@0.24.8): dependencies: @@ -3355,6 +2534,8 @@ snapshots: graphology-types: 0.24.8 obliterator: 2.0.5 + has-flag@4.0.0: {} + has-symbols@1.1.0: {} hasown@2.0.2: @@ -3363,6 +2544,8 @@ snapshots: hono@4.11.10: {} + html-escaper@2.0.2: {} + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -3383,41 +2566,55 @@ snapshots: inherits@2.0.4: {} - internmap@2.0.3: {} - ip-address@10.0.1: {} ipaddr.js@1.9.1: {} - is-docker@3.0.0: {} - is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-in-ssh@1.0.0: {} + is-promise@4.0.0: {} - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 + isexe@2.0.0: {} - is-promise@4.0.0: {} + istanbul-lib-coverage@3.2.2: {} - is-wsl@3.1.1: + istanbul-lib-report@3.0.1: dependencies: - is-inside-container: 1.0.0 + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 - isexe@2.0.0: {} + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 - jerrypick@1.1.2: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 - jiti@2.6.1: {} + jiti@2.6.1: + optional: true jose@6.1.3: {} - js-tokens@4.0.0: {} + js-tokens@10.0.0: {} js-tokens@9.0.1: {} @@ -3431,10 +2628,6 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - kapsule@1.16.3: - dependencies: - lodash-es: 4.17.23 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -3492,35 +2685,36 @@ snapshots: lightningcss-linux-x64-musl: 1.31.1 lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + optional: true locate-path@6.0.0: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - loupe@3.2.1: {} - lucide-react@0.574.0(react@19.2.4): - dependencies: - react: 19.2.4 + lru-cache@10.4.3: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.4 + math-intrinsics@1.1.0: {} media-typer@1.1.0: {} merge-descriptors@2.0.0: {} - meshoptimizer@0.22.0: {} - mime-db@1.54.0: {} mime-types@3.0.2: @@ -3531,10 +2725,16 @@ snapshots: dependencies: brace-expansion: 5.0.2 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.2 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 + minipass@7.1.3: {} + mnemonist@0.39.8: dependencies: obliterator: 2.0.5 @@ -3547,47 +2747,6 @@ snapshots: negotiator@1.0.0: {} - next@16.1.6(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@next/env': 16.1.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001770 - postcss: 8.4.31 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(react@19.2.4) - optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 - '@playwright/test': 1.58.2 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - ngraph.events@1.4.0: {} - - ngraph.forcelayout@3.3.1: - dependencies: - ngraph.events: 1.4.0 - ngraph.merge: 1.0.0 - ngraph.random: 1.2.0 - - ngraph.graph@20.1.2: - dependencies: - ngraph.events: 1.4.0 - - ngraph.merge@1.0.0: {} - - ngraph.random@1.2.0: {} - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -3602,15 +2761,6 @@ snapshots: dependencies: wrappy: 1.0.2 - open@11.0.0: - dependencies: - default-browser: 5.5.0 - define-lazy-prop: 3.0.0 - is-in-ssh: 1.0.0 - is-inside-container: 1.0.0 - powershell-utils: 0.1.0 - wsl-utils: 0.3.1 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -3628,6 +2778,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + pandemonium@2.4.1: dependencies: mnemonist: 0.39.8 @@ -3638,6 +2790,11 @@ snapshots: path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@8.3.0: {} pathe@2.0.3: {} @@ -3650,42 +2807,14 @@ snapshots: pkce-challenge@5.0.1: {} - playwright-core@1.58.2: {} - - playwright@1.58.2: - dependencies: - playwright-core: 1.58.2 - optionalDependencies: - fsevents: 2.3.2 - - polished@4.3.1: - dependencies: - '@babel/runtime': 7.28.6 - - postcss@8.4.31: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 - powershell-utils@0.1.0: {} - - preact@10.28.3: {} - prelude-ls@1.2.1: {} - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -3706,27 +2835,6 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 - react-dom@19.2.4(react@19.2.4): - dependencies: - react: 19.2.4 - scheduler: 0.27.0 - - react-force-graph-3d@1.29.1(react@19.2.4): - dependencies: - 3d-force-graph: 1.79.1 - prop-types: 15.8.1 - react: 19.2.4 - react-kapsule: 2.5.7(react@19.2.4) - - react-is@16.13.1: {} - - react-kapsule@2.5.7(react@19.2.4): - dependencies: - jerrypick: 1.1.2 - react: 19.2.4 - - react@19.2.4: {} - require-from-string@2.0.2: {} resolve-pkg-maps@1.0.0: {} @@ -3772,12 +2880,8 @@ snapshots: transitivePeerDependencies: - supports-color - run-applescript@7.1.0: {} - safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - semver@7.7.4: {} send@1.2.1: @@ -3807,38 +2911,6 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -3875,6 +2947,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} stackback@0.0.2: {} @@ -3883,56 +2957,42 @@ snapshots: std-env@3.10.0: {} - strip-literal@3.1.0: + string-width@4.2.3: dependencies: - js-tokens: 9.0.1 + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 - styled-jsx@5.1.6(react@19.2.4): + string-width@5.1.2: dependencies: - client-only: 0.0.1 - react: 19.2.4 + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 - swr@2.4.0(react@19.2.4): + strip-ansi@6.0.1: dependencies: - dequal: 2.0.3 - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) - - tailwind-merge@3.4.1: {} - - tailwindcss@4.2.0: {} + ansi-regex: 5.0.1 - tapable@2.3.0: {} + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 - three-forcegraph@1.43.1(three@0.183.0): + strip-literal@3.1.0: dependencies: - accessor-fn: 1.5.3 - d3-array: 3.2.4 - d3-force-3d: 3.0.6 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - data-bind-mapper: 1.0.3 - kapsule: 1.16.3 - ngraph.forcelayout: 3.3.1 - ngraph.graph: 20.1.2 - three: 0.183.0 - tinycolor2: 1.6.0 + js-tokens: 9.0.1 - three-render-objects@1.40.4(three@0.183.0): + supports-color@7.2.0: dependencies: - '@tweenjs/tween.js': 25.0.0 - accessor-fn: 1.5.3 - float-tooltip: 1.7.5 - kapsule: 1.16.3 - polished: 4.3.1 - three: 0.183.0 + has-flag: 4.0.0 - three@0.183.0: {} + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@0.3.2: {} tinyglobby@0.2.15: @@ -3952,8 +3012,6 @@ snapshots: dependencies: typescript: 5.9.3 - tslib@2.8.1: {} - tsx@4.21.0: dependencies: esbuild: 0.27.3 @@ -3992,10 +3050,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.4): - dependencies: - react: 19.2.4 - vary@1.1.2: {} vite-node@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0): @@ -4086,12 +3140,19 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 - wsl-utils@0.3.1: + wrap-ansi@8.1.0: dependencies: - is-wsl: 3.1.1 - powershell-utils: 0.1.0 + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} yocto-queue@0.1.0: {} diff --git a/postcss.config.mjs b/postcss.config.mjs deleted file mode 100644 index 61e3684..0000000 --- a/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - "@tailwindcss/postcss": {}, - }, -}; - -export default config; diff --git a/specs/active/2026-03-03-mcp-only.md b/specs/active/2026-03-03-mcp-only.md new file mode 100644 index 0000000..b71f55c --- /dev/null +++ b/specs/active/2026-03-03-mcp-only.md @@ -0,0 +1,243 @@ +--- +title: Drop Web UI & REST API — MCP Only +status: active +created: 2026-03-03 +estimate: 3h +tier: standard +--- + +# Drop Web UI & REST API — MCP Only + +## Context + +Package currently ships two modes: browser (Next.js 3D viz + REST API) and MCP stdio. Web UI adds ~1.8GB node_modules (next, react, three, playwright), 7000+ LOC, and maintenance drag. Agent-first strategy means MCP is the only consumer. Strip everything except the core pipeline (parser → graph → analyzer → MCP stdio). + +## Codebase Impact (MANDATORY) + +| Area | Impact | Detail | +|------|--------|--------| +| `app/` (13 API routes + page + layout + CSS) | DELETE | Next.js app router — all routes, pages, styles | +| `components/` (14 files) | DELETE | React components: graph-canvas, view-tabs, panels, legend, etc. | +| `hooks/` (3 files) | DELETE | React hooks: use-graph-data, use-graph-config, use-symbol-data | +| `lib/` (4 files) | DELETE | Client utils: views.ts, cluster-force.ts, types.ts, utils.ts | +| `public/` | DELETE | Static assets (if any) | +| `docs/screenshot-*.png` (3 files) | DELETE | Screenshots of 3D views | +| `next.config.ts` | DELETE | Next.js config | +| `postcss.config.mjs` | DELETE | PostCSS for Tailwind | +| `playwright.config.ts` | DELETE | E2E browser test config | +| `src/server/api-routes.test.ts` | DELETE | REST API test suite (~400 lines) | +| `src/server/graph-store.ts` | MODIFY | Remove `setGraph()`, `getProjectName()`, `getStaleness()` — keep only what MCP needs (`setIndexedHead`, `getIndexedHead`) | +| `src/cli.ts` | MODIFY | Remove `--mcp` flag (MCP is now default), `--port`, Next.js server logic, `createServer`/`next`/`open` imports. Simplify `runServer()` → direct `startMcpServer()` call | +| `src/mcp/index.ts` | MODIFY | Update import path if graph-store moves | +| `package.json` | MODIFY | Remove 12 deps + 8 devDeps, update scripts, files array, description, keywords | +| `tsconfig.json` | MODIFY | Remove `jsx`, `DOM` lib, `next` plugin, `paths`, `.next` includes | +| `eslint.config.js` | MODIFY | Remove React/Next.js rule overrides (lines 67-86), update ignores | +| `README.md` | MODIFY | Major rewrite — remove browser/API sections, focus MCP-only | +| `CLAUDE.md` | MODIFY | Update architecture, pipeline, conventions, file conventions, docs table | +| `docs/architecture.md` | MODIFY | Remove Express/Next.js layer, update pipeline diagram | +| `docs/mcp-tools.md` | MODIFY | Remove REST API references, HTTP MCP transport mention | +| `tests/mcp-parity.test.ts` | DELETE | MCP ↔ REST parity test — no REST to compare against | +| `tests/phase*.test.ts` | AFFECTED | Tests import `graph-store` — paths may change | +| `src/parser/`, `src/graph/`, `src/analyzer/` | UNAFFECTED | Core pipeline unchanged | +| `src/mcp/index.ts` | UNAFFECTED | MCP tool registration unchanged | +| `src/types/`, `src/persistence/`, `src/search/`, `src/impact/`, `src/process/`, `src/community/` | UNAFFECTED | All internal modules unchanged | + +**Files:** 0 create | 8 modify | ~40 delete | ~10 affected (test imports) +**Reuse:** Core pipeline entirely preserved. Only delivery layer changes. +**Breaking changes:** `--mcp` flag removed (becomes default). `--port` flag removed. No REST API. No browser mode. Existing `.mcp.json` configs using `--mcp` will still work (flag becomes no-op or just ignored). +**New dependencies:** None — this is pure subtraction. + +## User Journey (MANDATORY) + +### Primary Journey + +ACTOR: Developer integrating codebase-intelligence with LLM agent (Claude Code, Cursor, VS Code) +GOAL: Install and use codebase-intelligence as MCP server for code understanding +PRECONDITION: Node.js >= 18 installed, TypeScript codebase exists + +1. User runs `npx codebase-intelligence ./src` + → System parses codebase, builds graph, computes metrics + → User sees parse progress on stderr + +2. System starts MCP stdio server + → User's LLM agent connects via stdio + → Agent queries 15 MCP tools + +3. User queries codebase via agent + → System returns structured JSON responses + → Agent presents analysis to user + +POSTCONDITION: MCP server running, agent has full codebase intelligence + +### Error Journeys + +E1. Invalid path + Trigger: User provides non-existent directory + 1. User runs `npx codebase-intelligence ./nonexistent` + → System detects missing path + → User sees clear error message on stderr + Recovery: User provides correct path + +E2. Backward compatibility + Trigger: Existing config still uses `--mcp` flag + 1. User has `.mcp.json` with `["./src", "--mcp"]` args + → System accepts `--mcp` as no-op (or flag no longer defined = commander ignores unknown) + → MCP server starts normally + Recovery: No action needed — backward compatible + +### Edge Cases + +EC1. `--port` flag used: Error message explaining browser mode removed +EC2. Empty codebase (0 .ts files): Existing behavior preserved — parser returns empty, MCP tools return empty results + +## Acceptance Criteria (MANDATORY) + +### Must Have (BLOCKING — all must pass to ship) + +- [ ] AC-1: GIVEN user runs `npx codebase-intelligence ./src` (no flags) WHEN codebase exists THEN MCP stdio server starts (not browser) +- [ ] AC-2: GIVEN all web UI directories deleted WHEN `npm run build` runs THEN build succeeds with only `dist/` output +- [ ] AC-3: GIVEN 20 deps removed from package.json WHEN `pnpm install` runs THEN `node_modules` is ~1.5GB smaller +- [ ] AC-4: GIVEN CLI description updated WHEN user runs `--help` THEN no mention of browser, port, or web +- [ ] AC-5: GIVEN all 15 MCP tools WHEN invoked via stdio THEN responses identical to before (no regression) +- [ ] AC-6: GIVEN docs updated WHEN user reads README/docs THEN no reference to browser mode, REST API, or 3D views +- [ ] AC-7: GIVEN CLAUDE.md updated WHEN agent reads project instructions THEN architecture, pipeline, conventions reflect MCP-only + +### Error Criteria (BLOCKING — all must pass) + +- [ ] AC-E1: GIVEN existing `.mcp.json` with `--mcp` flag WHEN server starts THEN starts successfully (backward compat) +- [ ] AC-E2: GIVEN user passes `--port 3333` WHEN CLI parses args THEN clear error or ignored (not crash) + +### Should Have (ship without, fix soon) + +- [ ] AC-8: GIVEN invalid specs in specs/active/ WHEN migration done THEN UI-related specs moved to dropped/ + +## Scope + +- [ ] 1. Delete web UI directories: `app/`, `components/`, `hooks/`, `lib/`, `public/`, screenshots → AC-2 +- [ ] 2. Delete config files: `next.config.ts`, `postcss.config.mjs`, `playwright.config.ts` → AC-2 +- [ ] 3. Modify `src/cli.ts`: remove `--mcp`/`--port`, Next.js server logic, simplify to MCP-only → AC-1, AC-4, AC-E1, AC-E2 +- [ ] 4. Modify `src/server/graph-store.ts`: strip to MCP-needed exports only → AC-5 +- [ ] 5. Modify `package.json`: remove 20 deps, update scripts/files/description/keywords → AC-2, AC-3 +- [ ] 6. Modify `tsconfig.json`: remove JSX, DOM, Next.js plugin, path aliases → AC-2 +- [ ] 7. Modify `eslint.config.js`: remove React/Next.js overrides → AC-2 +- [ ] 8. Delete `src/server/api-routes.test.ts` and `tests/mcp-parity.test.ts` → AC-2 +- [ ] 9. Update `README.md`: rewrite for MCP-only (remove screenshots, browser, API sections) → AC-6 +- [ ] 10. Update `CLAUDE.md`: architecture, pipeline, conventions, file conventions, docs table → AC-7 +- [ ] 11. Update `docs/architecture.md`: remove Express/Next.js layer → AC-6 +- [ ] 12. Update `docs/mcp-tools.md`: remove REST references → AC-6 +- [ ] 13. Run quality gates: lint → typecheck → build → test → all pass → AC-2, AC-5 + +### Out of Scope + +- Renaming package (stays `codebase-intelligence`) +- Adding new MCP tools +- Changing MCP tool signatures or behavior +- Restructuring `src/` directories (e.g., moving graph-store) +- Moving/archiving invalid specs (should-have, not blocking) +- Updating CI/CD workflows (separate task) + +## Quality Checklist + +### Blocking (must pass to ship) + +- [ ] All Must Have ACs passing +- [ ] All Error Criteria ACs passing +- [ ] All scope items implemented +- [ ] No regressions in existing tests (parser, graph, analyzer, MCP) +- [ ] `npm run lint` passes (no references to deleted dirs) +- [ ] `npm run typecheck` passes (no React/Next.js type errors) +- [ ] `npm run build` produces only `dist/` (no `.next/`) +- [ ] `npm run test` passes (remaining test suites) +- [ ] No hardcoded secrets or credentials +- [ ] No orphan imports referencing deleted files + +### Advisory (should pass, not blocking) + +- [ ] `pnpm install` is < 500MB total +- [ ] All Should Have ACs passing +- [ ] Invalid specs archived to `specs/dropped/` + +## Risks + +| Risk | Impact | Likelihood | Mitigation | +|------|--------|------------|------------| +| Existing users rely on browser mode | MED | LOW | npm major version bump signals breaking change. But: MCP is the stated primary use case. | +| `--mcp` flag removal breaks `.mcp.json` configs | HIGH | MED | Keep `--mcp` as accepted but ignored flag (no-op) via commander `.option()` | +| Test suites reference graph-store paths that change | MED | HIGH | Update import paths in `tests/phase*.test.ts` if graph-store moves | +| Removing `DOM` from tsconfig breaks existing src/ code | LOW | LOW | grep for DOM APIs in `src/` — none expected since server-side only | +| `publish:npm` script references `next build` | HIGH | HIGH | Update script before running — or build will fail | + +**Kill criteria:** If MCP tool responses differ after changes (regression), stop and investigate before continuing. + +## State Machine + +**Status**: N/A — Stateless feature + +**Rationale**: This is a deletion/simplification task. No persistent state transitions. File deletions → config updates → docs updates → validate. + +## Analysis + +### Assumptions Challenged + +| Assumption | Evidence For | Evidence Against | Verdict | +|------------|-------------|-----------------|---------| +| No users depend on browser mode | MCP is primary documented use case; npm package description leads with MCP | Browser mode is default (`npx` without `--mcp` opens browser); could have casual users | RISKY — mitigate with semver major bump | +| graph-store can be simplified without breaking MCP | MCP only imports `getIndexedHead()` from graph-store | `tests/phase*.test.ts` import `setGraph`, `setIndexedHead` from graph-store — must preserve those | VALID — but preserve test-needed exports | +| `--mcp` flag can be silently removed | Clean approach, less confusion | Existing `.mcp.json` files include `--mcp` in args — commander would error on unknown option | WRONG — keep `--mcp` as no-op flag for backward compat | + +### Blind Spots + +1. **[integration]** Existing CI/CD workflows (`.github/workflows/`) may reference `next build`, Playwright, or browser-specific steps + Why it matters: CI will break on next push if not updated + +2. **[packaging]** `files` array in package.json includes `app`, `components`, `hooks`, `lib`, `public` — npm publish would fail or publish stale dirs + Why it matters: npm package would be broken + +3. **[testing]** `tests/phase*.test.ts` all import from `src/server/graph-store.ts` — if we move/restructure, 8 test files break + Why it matters: test suite breaks, blocking quality gates + +### Failure Hypotheses + +| IF | THEN | BECAUSE | Severity | Mitigation | +|----|------|---------|----------|------------| +| `--mcp` removed from commander options | `.mcp.json` configs with `--mcp` flag cause startup error | Commander throws on unknown options | HIGH | Keep `--mcp` as hidden/no-op option | +| `DOM` removed from tsconfig lib | Compilation fails | Some `src/` file uses DOM API (e.g., `console.log` needs no DOM, but other APIs might) | MED | Grep for DOM usage in `src/` before removing | +| `tests/phase*.test.ts` paths not updated | Test suite fails | graph-store import path changes | HIGH | Keep graph-store at same path, only strip unused exports | + +### The Real Question + +Confirmed — spec solves the right problem. The package is evolving from "visualization tool with MCP" to "MCP analysis engine." Removing the web layer reduces maintenance surface by ~60%, npm package size by ~90%, and install time by ~80%. The core value (parser → graph → analyzer → MCP) is entirely preserved. + +### Open Items + +- [risk] CI workflows may reference Next.js/Playwright → explore (check `.github/workflows/`) +- [question] Keep `--mcp` as no-op or remove entirely? → update spec (keep as no-op per failure hypothesis) +- [gap] Should this be a major version bump (2.0.0)? → question (removing default browser mode is breaking) +- [improvement] Consider renaming `src/server/graph-store.ts` to `src/graph-store.ts` → no action (out of scope, minimize churn) + +## Notes + + + +## Progress + +| # | Scope Item | Status | Iteration | +|---|-----------|--------|-----------| +| 1 | Delete web UI dirs | [x] Complete | 1 | +| 2 | Delete config files | [x] Complete | 1 | +| 3 | Modify cli.ts | [x] Complete | 1 | +| 4 | Modify graph-store.ts | [x] Complete | 1 | +| 5 | Update package.json | [x] Complete | 1 | +| 6 | Update tsconfig + eslint | [x] Complete | 1 | +| 7 | Delete obsolete tests | [x] Complete | 1 | +| 8 | Rewrite README.md | [x] Complete | 1 | +| 9 | Update CLAUDE.md | [x] Complete | 1 | +| 10 | Update docs/ | [x] Complete | 1 | +| 11 | Quality gates | [x] Complete | 1 | + +## Timeline + +| Action | Timestamp | Duration | Notes | +|--------|-----------|----------|-------| +| plan | 2026-03-03T00:00:00Z | - | Created | +| ship | 2026-03-03T00:00:00Z | - | All scope complete, gates green | diff --git a/src/cli.ts b/src/cli.ts index e539510..aed659d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,12 +1,5 @@ #!/usr/bin/env node -let serverRunning = false; - -process.on("SIGTERM", () => { - if (serverRunning) return; - process.exit(0); -}); - process.on("SIGINT", () => { process.exit(0); }); @@ -19,18 +12,14 @@ process.on("uncaughtException", (err) => { import fs from "fs"; import path from "path"; import { execSync } from "child_process"; -import { fileURLToPath } from "url"; -import { createServer } from "http"; import { Command } from "commander"; import { parseCodebase } from "./parser/index.js"; import { buildGraph } from "./graph/index.js"; import { analyzeGraph } from "./analyzer/index.js"; import { startMcpServer } from "./mcp/index.js"; -import { setGraph, setIndexedHead } from "./server/graph-store.js"; +import { setIndexedHead } from "./server/graph-store.js"; import { exportGraph, importGraph } from "./persistence/index.js"; -import type { CodebaseGraph } from "./types/index.js"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); const INDEX_DIR_NAME = ".code-visualizer"; const program = new Command(); @@ -52,7 +41,6 @@ function getHeadHash(targetPath: string): string { interface CliOptions { mcp?: boolean; - port: string; index?: boolean; force?: boolean; status?: boolean; @@ -61,11 +49,10 @@ interface CliOptions { program .name("codebase-intelligence") - .description("3D interactive codebase visualization with MCP integration") - .version("1.0.0") - .argument("", "Path to the codebase directory to visualize") - .option("--mcp", "Start as MCP stdio server (no browser, no HTTP)") - .option("--port ", "Web server port (browser mode only)", "3333") + .description("Codebase analysis engine with MCP integration for LLM-assisted code understanding") + .version("1.1.0") + .argument("", "Path to the TypeScript codebase to analyze") + .option("--mcp", "Start as MCP stdio server (accepted for backward compatibility)") .option("--index", "Persist graph index to .code-visualizer/") .option("--force", "Re-index even if HEAD unchanged") .option("--status", "Print index status and exit") @@ -112,7 +99,7 @@ program console.log(`Using cached index (HEAD: ${headHash.slice(0, 7)})`); const codebaseGraph = cached.graph; setIndexedHead(cached.headHash); - await runServer(targetPath, codebaseGraph, options); + await startMcpServer(codebaseGraph); return; } } @@ -141,7 +128,7 @@ program console.log(`Index saved to ${indexDir}`); } - await runServer(targetPath, codebaseGraph, options); + await startMcpServer(codebaseGraph); } catch (error) { if (error instanceof Error) { console.error(error.message); @@ -152,67 +139,4 @@ program } }); -async function runServer( - targetPath: string, - codebaseGraph: CodebaseGraph, - options: CliOptions, -): Promise { - if (options.mcp) { - await startMcpServer(codebaseGraph); - return; - } - - const port = parseInt(options.port, 10); - let projectName = path.basename(path.resolve(targetPath)); - try { - const pkg = JSON.parse(fs.readFileSync(path.resolve(targetPath, "package.json"), "utf-8")) as { name?: string }; - if (pkg.name) projectName = pkg.name; - } catch { /* no package.json — use directory name */ } - - setGraph(codebaseGraph, projectName); - - const projectDir = path.resolve(__dirname, ".."); - const isDev = import.meta.url.endsWith(".ts"); - const next = (await import("next")).default; - const app = next({ dev: isDev, dir: projectDir }); - const handle = app.getRequestHandler(); - - await app.prepare(); - - const server = createServer((req, res) => { - handle(req, res).catch((err: unknown) => { - const msg = err instanceof Error ? err.message : String(err); - process.stderr.write(`Request error: ${req.url} — ${msg}\n`); - if (!res.headersSent) { - res.statusCode = 500; - res.end("Internal Server Error"); - } - }); - }); - - await new Promise((resolve, reject) => { - let attempts = 0; - const maxAttempts = 5; - - function attempt(currentPort: number): void { - attempts++; - server.listen(currentPort, "0.0.0.0", () => { resolve(); }); - server.on("error", (err: NodeJS.ErrnoException) => { - if (err.code === "EADDRINUSE" && attempts < maxAttempts) { - console.warn(`Port ${currentPort} in use, trying ${currentPort + 1}...`); - attempt(currentPort + 1); - } else { - reject(err); - } - }); - } - - attempt(port); - }); - - serverRunning = true; - const actualPort = (server.address() as { port: number }).port; - console.log(`3D map ready at http://localhost:${actualPort}`); -} - program.parse(); diff --git a/src/mcp/transport.ts b/src/mcp/transport.ts deleted file mode 100644 index 8a732ce..0000000 --- a/src/mcp/transport.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import http from "node:http"; -import type { CodebaseGraph } from "../types/index.js"; -import { registerTools } from "./index.js"; - -/** Create an HTTP server that serves MCP tools via StreamableHTTP transport (stateless). */ -export async function createHttpMcpServer(graph: CodebaseGraph, port: number): Promise { - const httpServer = http.createServer((req, res) => { - if (req.url === "/mcp" && (req.method === "POST" || req.method === "GET" || req.method === "DELETE")) { - void (async () => { - const server = new McpServer({ - name: "codebase-intelligence-http", - version: "0.1.0", - }); - registerTools(server, graph); - - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }); - - res.on("close", () => { - void transport.close(); - }); - - await server.connect(transport); - - const body = await new Promise((resolve) => { - let data = ""; - req.on("data", (chunk: Buffer) => { data += chunk.toString(); }); - req.on("end", () => { resolve(data); }); - }); - - await transport.handleRequest(req, res, body ? JSON.parse(body) as unknown : undefined); - })(); - } else { - res.writeHead(404); - res.end("Not found"); - } - }); - - await new Promise((resolve) => { - httpServer.listen(port, "127.0.0.1", () => { resolve(); }); - }); - - return httpServer; -} diff --git a/src/server/api-routes.test.ts b/src/server/api-routes.test.ts deleted file mode 100644 index e8f2d82..0000000 --- a/src/server/api-routes.test.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import fs from "fs"; -import os from "os"; -import path from "path"; -import { parseCodebase } from "../parser/index.js"; -import { buildGraph } from "../graph/index.js"; -import { analyzeGraph } from "../analyzer/index.js"; -import { setGraph, getGraph, getProjectName } from "./graph-store.js"; - -let projectDir: string; - -beforeAll(() => { - projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "cv-api-test-")); - - fs.writeFileSync( - path.join(projectDir, "utils.ts"), - `export function add(a: number, b: number): number { return a + b; } -export const VERSION = "1.0.0"; -`, - ); - - fs.writeFileSync( - path.join(projectDir, "types.ts"), - `export interface User { id: string; name: string; } -export type UserList = User[]; -`, - ); - - fs.writeFileSync( - path.join(projectDir, "main.ts"), - `import { add, VERSION } from "./utils"; -import type { User } from "./types"; -export function greet(user: User): string { - return \`Hello \${user.name}, \${VERSION}, \${add(1, 2)}\`; -} -export default function run(): void { console.log("running"); } -`, - ); - - fs.mkdirSync(path.join(projectDir, "lib")); - fs.writeFileSync( - path.join(projectDir, "lib", "helper.ts"), - `import { add } from "../utils"; -export class Calculator { sum(a: number, b: number): number { return add(a, b); } } -`, - ); - - const files = parseCodebase(projectDir); - const built = buildGraph(files); - const graph = analyzeGraph(built, files); - setGraph(graph, "test-project"); -}); - -afterAll(() => { - fs.rmSync(projectDir, { recursive: true, force: true }); -}); - -describe("graph-store", () => { - it("returns the stored graph", () => { - const graph = getGraph(); - expect(graph.nodes.length).toBeGreaterThan(0); - expect(graph.edges.length).toBeGreaterThan(0); - expect(graph.stats.totalFiles).toBeGreaterThan(0); - }); - - it("returns the project name", () => { - expect(getProjectName()).toBe("test-project"); - }); -}); - -describe("GET /api/graph", () => { - it("returns nodes, edges, and stats", async () => { - const { GET } = await import("../../app/api/graph/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data).toHaveProperty("nodes"); - expect(data).toHaveProperty("edges"); - expect(data).toHaveProperty("stats"); - expect(data.nodes.length).toBeGreaterThan(0); - expect(data.stats.totalFiles).toBeGreaterThan(0); - }); - - it("includes metrics on file nodes", async () => { - const { GET } = await import("../../app/api/graph/route.js"); - const response = GET(); - const data = await response.json(); - const node = data.nodes[0]; - - expect(node).toHaveProperty("id"); - expect(node).toHaveProperty("label"); - expect(node).toHaveProperty("path"); - expect(node).toHaveProperty("loc"); - expect(node).toHaveProperty("module"); - expect(node).toHaveProperty("pageRank"); - expect(node).toHaveProperty("coupling"); - expect(node).toHaveProperty("fanIn"); - expect(node).toHaveProperty("fanOut"); - expect(node).toHaveProperty("functions"); - }); - - it("includes edge properties", async () => { - const { GET } = await import("../../app/api/graph/route.js"); - const response = GET(); - const data = await response.json(); - - if (data.edges.length > 0) { - const edge = data.edges[0]; - expect(edge).toHaveProperty("source"); - expect(edge).toHaveProperty("target"); - expect(edge).toHaveProperty("symbols"); - expect(edge).toHaveProperty("weight"); - } - }); -}); - -describe("GET /api/meta", () => { - it("returns project name", async () => { - const { GET } = await import("../../app/api/meta/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data.projectName).toBe("test-project"); - }); -}); - -describe("GET /api/ping", () => { - it("returns ok: true", async () => { - const { GET } = await import("../../app/api/ping/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data.ok).toBe(true); - }); -}); - -describe("GET /api/modules", () => { - it("returns module metrics", async () => { - const { GET } = await import("../../app/api/modules/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data).toHaveProperty("modules"); - expect(Array.isArray(data.modules)).toBe(true); - }); -}); - -describe("GET /api/forces", () => { - it("returns force analysis data", async () => { - const { GET } = await import("../../app/api/forces/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data).toHaveProperty("moduleCohesion"); - expect(data).toHaveProperty("tensionFiles"); - expect(data).toHaveProperty("bridgeFiles"); - expect(data).toHaveProperty("extractionCandidates"); - expect(data).toHaveProperty("summary"); - }); -}); - -describe("GET /api/hotspots", () => { - it("returns hotspots sorted by score", async () => { - const { GET } = await import("../../app/api/hotspots/route.js"); - const request = new Request("http://localhost/api/hotspots?metric=coupling&limit=5"); - const response = GET(request); - const data = await response.json(); - - expect(data).toHaveProperty("metric", "coupling"); - expect(data).toHaveProperty("hotspots"); - expect(data.hotspots.length).toBeLessThanOrEqual(5); - }); - - it("defaults to coupling metric", async () => { - const { GET } = await import("../../app/api/hotspots/route.js"); - const request = new Request("http://localhost/api/hotspots"); - const response = GET(request); - const data = await response.json(); - - expect(data).toHaveProperty("metric", "coupling"); - expect(data).toHaveProperty("hotspots"); - }); -}); - -describe("GET /api/file/[...path]", () => { - it("returns file details for valid path", async () => { - const { GET } = await import("../../app/api/file/[...path]/route.js"); - const graph = getGraph(); - const firstFile = graph.nodes.find((n) => n.type === "file"); - if (!firstFile) throw new Error("No files in graph"); - - const segments = firstFile.path.split("/"); - const request = new Request(`http://localhost/api/file/${firstFile.path}`); - const response = await GET(request, { params: Promise.resolve({ path: segments }) }); - const data = await response.json(); - - expect(data).toHaveProperty("path"); - expect(data).toHaveProperty("metrics"); - expect(data.metrics).toHaveProperty("pageRank"); - expect(data.metrics).toHaveProperty("fanIn"); - }); - - it("returns 404 for unknown file", async () => { - const { GET } = await import("../../app/api/file/[...path]/route.js"); - const request = new Request("http://localhost/api/file/nonexistent.ts"); - const response = await GET(request, { params: Promise.resolve({ path: ["nonexistent.ts"] }) }); - - expect(response.status).toBe(404); - }); -}); - -describe("GET /api/groups", () => { - it("returns object with groups array", async () => { - const { GET } = await import("../../app/api/groups/route.js"); - const response = GET(); - const data = await response.json() as { groups: Array> }; - - expect(Array.isArray(data.groups)).toBe(true); - }); - - it("each group has required fields", async () => { - const { GET } = await import("../../app/api/groups/route.js"); - const response = GET(); - const data = await response.json() as { groups: Array> }; - - if (data.groups.length > 0) { - const group = data.groups[0]; - expect(group).toHaveProperty("name"); - expect(group).toHaveProperty("files"); - expect(group).toHaveProperty("loc"); - expect(group).toHaveProperty("importance"); - expect(group).toHaveProperty("fanIn"); - expect(group).toHaveProperty("fanOut"); - expect(group).toHaveProperty("color"); - } - }); - - it("groups are sorted by importance descending", async () => { - const { GET } = await import("../../app/api/groups/route.js"); - const response = GET(); - const data = await response.json() as { groups: Array<{ importance: number }> }; - - for (let i = 1; i < data.groups.length; i++) { - expect(data.groups[i - 1].importance).toBeGreaterThanOrEqual(data.groups[i].importance); - } - }); -}); - -describe("POST /api/mcp", () => { - it("lists available tools on GET", async () => { - const { GET } = await import("../../app/api/mcp/route.js"); - const response = GET(); - const data = await response.json(); - - expect(data).toHaveProperty("tools"); - expect(data.tools.length).toBe(8); - expect(data.tools.map((t: { name: string }) => t.name)).toContain("codebase_overview"); - expect(data.tools.map((t: { name: string }) => t.name)).toContain("get_groups"); - }); - - it("invokes codebase_overview tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "codebase_overview" }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(data).toHaveProperty("content"); - expect(data.content[0].type).toBe("text"); - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("totalFiles"); - expect(parsed).toHaveProperty("modules"); - }); - - it("invokes file_context tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const graph = getGraph(); - const firstFile = graph.nodes.find((n) => n.type === "file"); - if (!firstFile) throw new Error("No files in graph"); - - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "file_context", params: { filePath: firstFile.path } }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(data.isError).toBeUndefined(); - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("path"); - expect(parsed).toHaveProperty("metrics"); - }); - - it("invokes find_hotspots tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "find_hotspots", params: { metric: "coupling", limit: 3 } }), - }); - const response = await POST(request); - const data = await response.json(); - - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("metric", "coupling"); - expect(parsed).toHaveProperty("hotspots"); - expect(parsed.hotspots.length).toBeLessThanOrEqual(3); - }); - - it("invokes get_module_structure tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "get_module_structure" }), - }); - const response = await POST(request); - const data = await response.json(); - - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("modules"); - expect(parsed).toHaveProperty("circularDeps"); - }); - - it("invokes analyze_forces tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "analyze_forces" }), - }); - const response = await POST(request); - const data = await response.json(); - - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("moduleCohesion"); - expect(parsed).toHaveProperty("summary"); - }); - - it("invokes find_dead_exports tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "find_dead_exports" }), - }); - const response = await POST(request); - const data = await response.json(); - - const parsed = JSON.parse(data.content[0].text); - expect(parsed).toHaveProperty("totalDeadExports"); - expect(parsed).toHaveProperty("files"); - }); - - it("invokes get_groups tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "get_groups" }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(data).toHaveProperty("content"); - expect(data.content[0].type).toBe("text"); - }); - - it("returns error for unknown tool", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tool: "nonexistent_tool" }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(data.isError).toBe(true); - }); - - it("returns 400 for missing tool field", async () => { - const { POST } = await import("../../app/api/mcp/route.js"); - const request = new Request("http://localhost/api/mcp", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - const response = await POST(request); - - expect(response.status).toBe(400); - }); -}); diff --git a/src/server/graph-store.ts b/src/server/graph-store.ts index 1584697..dbd86a2 100644 --- a/src/server/graph-store.ts +++ b/src/server/graph-store.ts @@ -1,23 +1,14 @@ import type { CodebaseGraph } from "../types/index.js"; -interface Staleness { - stale: boolean; - indexedHash: string; - currentHash: string; -} - declare global { var __codebaseGraph: CodebaseGraph | undefined; - var __projectName: string | undefined; - var __indexedHeadHash: string | undefined; } -export function setGraph(graph: CodebaseGraph, projectName: string): void { +export function setGraph(graph: CodebaseGraph): void { globalThis.__codebaseGraph = graph; - globalThis.__projectName = projectName; } export function getGraph(): CodebaseGraph { @@ -27,10 +18,6 @@ export function getGraph(): CodebaseGraph { return globalThis.__codebaseGraph; } -export function getProjectName(): string { - return globalThis.__projectName ?? "unknown"; -} - export function setIndexedHead(hash: string): void { globalThis.__indexedHeadHash = hash; } @@ -38,12 +25,3 @@ export function setIndexedHead(hash: string): void { export function getIndexedHead(): string { return globalThis.__indexedHeadHash ?? ""; } - -export function getStaleness(currentHash: string): Staleness { - const indexedHash = globalThis.__indexedHeadHash ?? ""; - return { - stale: indexedHash !== currentHash, - indexedHash, - currentHash, - }; -} diff --git a/tests/cluster-force.test.ts b/tests/cluster-force.test.ts deleted file mode 100644 index ba85b82..0000000 --- a/tests/cluster-force.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { createClusterForce } from "@/lib/cluster-force"; - -function dist(a: Record, b: Record): number { - const dx = (a.x as number) - (b.x as number); - const dy = (a.y as number) - (b.y as number); - const dz = (a.z as number) - (b.z as number); - return Math.sqrt(dx * dx + dy * dy + dz * dz); -} - -function avgPairwiseDistance(nodes: Array>): number { - let sum = 0; - let count = 0; - for (let i = 0; i < nodes.length; i++) { - for (let j = i + 1; j < nodes.length; j++) { - sum += dist(nodes[i], nodes[j]); - count++; - } - } - return count > 0 ? sum / count : 0; -} - -function makeNodes(groups: Record, spread: number): Array> { - const nodes: Array> = []; - for (const [group, count] of Object.entries(groups)) { - for (let i = 0; i < count; i++) { - nodes.push({ - id: `${group}-${i}`, - group, - x: (Math.random() - 0.5) * spread, - y: (Math.random() - 0.5) * spread, - z: (Math.random() - 0.5) * spread, - vx: 0, vy: 0, vz: 0, - }); - } - } - return nodes; -} - -function simulateTicks( - force: { (alpha: number): void; initialize: (nodes: Array>) => void }, - nodes: Array>, - ticks: number, -): void { - force.initialize(nodes); - for (let t = 0; t < ticks; t++) { - const alpha = 1 - t / ticks; - force(alpha); - for (const node of nodes) { - (node.x as number) += (node.vx as number); - (node.y as number) += (node.vy as number); - (node.z as number) += (node.vz as number); - node.vx = (node.vx as number) * 0.6; - node.vy = (node.vy as number) * 0.6; - node.vz = (node.vz as number) * 0.6; - } - } -} - -describe("createClusterForce", () => { - it("produces intra-group distance < inter-group distance after simulation", () => { - const nodes = makeNodes({ services: 5, models: 5, utils: 5 }, 200); - const force = createClusterForce((n) => n.group as string, 0.5); - - simulateTicks(force, nodes, 100); - - const groups: Record>> = {}; - for (const n of nodes) { - const g = n.group as string; - (groups[g] ??= []).push(n); - } - - const intraDistances: number[] = []; - for (const groupNodes of Object.values(groups)) { - intraDistances.push(avgPairwiseDistance(groupNodes)); - } - const avgIntra = intraDistances.reduce((a, b) => a + b, 0) / intraDistances.length; - - const interNodes: Array> = []; - const groupNames = Object.keys(groups); - for (let i = 0; i < groupNames.length; i++) { - for (let j = i + 1; j < groupNames.length; j++) { - for (const a of groups[groupNames[i]]) { - for (const b of groups[groupNames[j]]) { - interNodes.push(a, b); - } - } - } - } - let interSum = 0; - let interCount = 0; - for (let i = 0; i < groupNames.length; i++) { - for (let j = i + 1; j < groupNames.length; j++) { - for (const a of groups[groupNames[i]]) { - for (const b of groups[groupNames[j]]) { - interSum += dist(a, b); - interCount++; - } - } - } - } - const avgInter = interSum / interCount; - - expect(avgIntra).toBeLessThan(avgInter); - }); - - it("strength=0 applies no force (nodes unchanged)", () => { - const nodes = makeNodes({ a: 4, b: 4 }, 200); - const snapshotBefore = nodes.map((n) => ({ x: n.x, y: n.y, z: n.z })); - const force = createClusterForce((n) => n.group as string, 0); - - simulateTicks(force, nodes, 50); - - for (let i = 0; i < nodes.length; i++) { - expect(nodes[i].x).toBe(snapshotBefore[i].x); - expect(nodes[i].y).toBe(snapshotBefore[i].y); - expect(nodes[i].z).toBe(snapshotBefore[i].z); - } - }); - - it("strength() method updates force behavior", () => { - const nodes = makeNodes({ a: 5, b: 5 }, 200); - const force = createClusterForce((n) => n.group as string, 0); - - simulateTicks(force, nodes, 20); - const afterZero = nodes.map((n) => ({ x: n.x, y: n.y, z: n.z })); - - force.strength(0.8); - simulateTicks(force, nodes, 50); - - let moved = false; - for (let i = 0; i < nodes.length; i++) { - if ( - nodes[i].x !== afterZero[i].x || - nodes[i].y !== afterZero[i].y || - nodes[i].z !== afterZero[i].z - ) { - moved = true; - break; - } - } - expect(moved).toBe(true); - }); - - it("single-node clusters are skipped (count < 2)", () => { - const nodes: Array> = [ - { id: "lone", group: "solo", x: 100, y: 100, z: 100, vx: 0, vy: 0, vz: 0 }, - { id: "a1", group: "team", x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0 }, - { id: "a2", group: "team", x: 50, y: 50, z: 50, vx: 0, vy: 0, vz: 0 }, - ]; - const force = createClusterForce((n) => n.group as string, 0.5); - - simulateTicks(force, nodes, 30); - - expect(nodes[0].x).toBe(100); - expect(nodes[0].y).toBe(100); - expect(nodes[0].z).toBe(100); - }); - - it("distanceMin=5 prevents jitter when nodes converge near centroid", () => { - const nodes: Array> = [ - { id: "a1", group: "g", x: 1, y: 1, z: 1, vx: 0, vy: 0, vz: 0 }, - { id: "a2", group: "g", x: 3, y: 3, z: 3, vx: 0, vy: 0, vz: 0 }, - ]; - const force = createClusterForce((n) => n.group as string, 1.0); - force.initialize(nodes); - - force(1.0); - - expect(nodes[0].vx).toBe(0); - expect(nodes[1].vx).toBe(0); - }); -}); diff --git a/tests/graph-store.test.ts b/tests/graph-store.test.ts new file mode 100644 index 0000000..5e095dc --- /dev/null +++ b/tests/graph-store.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { setGraph, getGraph, setIndexedHead, getIndexedHead } from "../src/server/graph-store.js"; +import { getFixturePipeline } from "./helpers/pipeline.js"; + +beforeEach(() => { + globalThis.__codebaseGraph = undefined; + globalThis.__indexedHeadHash = undefined; +}); + +describe("graph-store", () => { + describe("setGraph / getGraph", () => { + it("throws when graph not initialized", () => { + expect(() => getGraph()).toThrow("Graph not initialized"); + }); + + it("returns graph after setGraph", () => { + const { codebaseGraph } = getFixturePipeline(); + setGraph(codebaseGraph); + const result = getGraph(); + expect(result).toBe(codebaseGraph); + expect(result.nodes.length).toBeGreaterThan(0); + }); + }); + + describe("setIndexedHead / getIndexedHead", () => { + it("returns empty string when not set", () => { + expect(getIndexedHead()).toBe(""); + }); + + it("returns hash after setIndexedHead", () => { + setIndexedHead("abc123"); + expect(getIndexedHead()).toBe("abc123"); + }); + + it("overwrites previous value", () => { + setIndexedHead("first"); + setIndexedHead("second"); + expect(getIndexedHead()).toBe("second"); + }); + }); +}); diff --git a/tests/mcp-parity.test.ts b/tests/mcp-parity.test.ts deleted file mode 100644 index df4778e..0000000 --- a/tests/mcp-parity.test.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { describe, it, expect, beforeAll } from "vitest"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { getFixturePipeline } from "./helpers/pipeline.js"; -import { registerTools } from "../src/mcp/index.js"; -import type { CodebaseGraph } from "../src/types/index.js"; - -let graph: CodebaseGraph; -let server: McpServer; - -interface ToolResult { - content: Array<{ type: string; text: string }>; - isError?: boolean; -} - -interface RegisteredTool { - handler: (args: Record) => Promise; -} - -type ToolRegistry = Record; - -function getTools(): ToolRegistry { - return (server as unknown as { _registeredTools: ToolRegistry })._registeredTools; -} - -async function callTool(name: string, args: Record = {}): Promise { - const tools = getTools(); - const tool = tools[name] as RegisteredTool | undefined; - if (tool === undefined) throw new Error(`Tool not found: ${name}`); - return tool.handler(args); -} - -function parseResult(result: ToolResult): unknown { - return JSON.parse(result.content[0].text); -} - -beforeAll(() => { - const pipeline = getFixturePipeline(); - graph = pipeline.codebaseGraph; - server = new McpServer({ name: "test", version: "0.0.1" }); - registerTools(server, graph); -}); - -describe("symbol_context enhancements", () => { - it("returns pageRank, betweenness, loc, type fields", async () => { - const symbolName = graph.symbolNodes[0]?.name; - if (!symbolName) return; - - const result = await callTool("symbol_context", { name: symbolName }); - const data = parseResult(result) as Record; - - expect(data).toHaveProperty("pageRank"); - expect(data).toHaveProperty("betweenness"); - expect(data).toHaveProperty("loc"); - expect(data).toHaveProperty("type"); - expect(typeof data.pageRank).toBe("number"); - expect(typeof data.betweenness).toBe("number"); - expect(typeof data.loc).toBe("number"); - expect(typeof data.type).toBe("string"); - }); - - it("includes confidence on callers", async () => { - const symWithCallers = [...graph.symbolMetrics.values()].find((s) => s.fanIn > 0); - if (!symWithCallers) return; - - const result = await callTool("symbol_context", { name: symWithCallers.name }); - const data = parseResult(result) as { callers: Array<{ confidence: string }> }; - - if (data.callers.length > 0) { - expect(data.callers[0]).toHaveProperty("confidence"); - expect(["type-resolved", "text-inferred"]).toContain(data.callers[0].confidence); - } - }); - - it("includes confidence on callees", async () => { - const symWithCallees = [...graph.symbolMetrics.values()].find((s) => s.fanOut > 0); - if (!symWithCallees) return; - - const result = await callTool("symbol_context", { name: symWithCallees.name }); - const data = parseResult(result) as { callees: Array<{ confidence: string }> }; - - if (data.callees.length > 0) { - expect(data.callees[0]).toHaveProperty("confidence"); - expect(["type-resolved", "text-inferred"]).toContain(data.callees[0].confidence); - } - }); - - it("returns isDefault and complexity fields", async () => { - const symbolName = graph.symbolNodes[0]?.name; - if (!symbolName) return; - - const result = await callTool("symbol_context", { name: symbolName }); - const data = parseResult(result) as Record; - - expect(data).toHaveProperty("isDefault"); - expect(data).toHaveProperty("complexity"); - expect(typeof data.isDefault).toBe("boolean"); - expect(typeof data.complexity).toBe("number"); - }); - - it("returns error for unknown symbol", async () => { - const result = await callTool("symbol_context", { name: "NonExistentSymbol12345" }); - expect(result.isError).toBe(true); - const data = parseResult(result) as { error: string }; - expect(data.error).toContain("Symbol not found"); - }); -}); - -describe("file_context edge metadata", () => { - it("imports include isTypeOnly and weight", async () => { - const filePath = graph.nodes.find((n) => n.type === "file")?.id; - if (!filePath) return; - - const result = await callTool("file_context", { filePath }); - const data = parseResult(result) as { - imports: Array<{ from: string; symbols: string[]; isTypeOnly: boolean; weight: number }>; - }; - - for (const imp of data.imports) { - expect(imp).toHaveProperty("isTypeOnly"); - expect(imp).toHaveProperty("weight"); - expect(typeof imp.isTypeOnly).toBe("boolean"); - expect(typeof imp.weight).toBe("number"); - } - }); - - it("dependents include isTypeOnly and weight", async () => { - const fileWithDeps = [...graph.fileMetrics.entries()].find(([, m]) => m.fanIn > 0); - if (!fileWithDeps) return; - - const result = await callTool("file_context", { filePath: fileWithDeps[0] }); - const data = parseResult(result) as { - dependents: Array<{ path: string; symbols: string[]; isTypeOnly: boolean; weight: number }>; - }; - - if (data.dependents.length > 0) { - expect(data.dependents[0]).toHaveProperty("isTypeOnly"); - expect(data.dependents[0]).toHaveProperty("weight"); - expect(typeof data.dependents[0].isTypeOnly).toBe("boolean"); - expect(typeof data.dependents[0].weight).toBe("number"); - } - }); -}); - -describe("analyze_forces threshold params", () => { - it("defaults match original hardcoded behavior", async () => { - const result = await callTool("analyze_forces", {}); - const data = parseResult(result) as { - moduleCohesion: Array<{ cohesion: number; verdict: string }>; - tensionFiles: unknown[]; - }; - - expect(data.moduleCohesion.length).toBeGreaterThan(0); - for (const m of data.moduleCohesion) { - if (m.cohesion >= 0.6) expect(m.verdict).toBe("COHESIVE"); - } - }); - - it("high tension threshold filters out tension files", async () => { - const defaultResult = await callTool("analyze_forces", {}); - const defaultData = parseResult(defaultResult) as { tensionFiles: unknown[] }; - - const strictResult = await callTool("analyze_forces", { tensionThreshold: 0.99 }); - const strictData = parseResult(strictResult) as { tensionFiles: unknown[] }; - - expect(strictData.tensionFiles.length).toBeLessThanOrEqual(defaultData.tensionFiles.length); - }); - - it("high escape threshold filters extraction candidates", async () => { - const defaultResult = await callTool("analyze_forces", {}); - const defaultData = parseResult(defaultResult) as { extractionCandidates: unknown[] }; - - const strictResult = await callTool("analyze_forces", { escapeThreshold: 0.99 }); - const strictData = parseResult(strictResult) as { extractionCandidates: unknown[] }; - - expect(strictData.extractionCandidates.length).toBeLessThanOrEqual(defaultData.extractionCandidates.length); - }); -}); - -describe("detect_changes enrichment", () => { - it("response includes fileRiskMetrics array", async () => { - const result = await callTool("detect_changes", { scope: "all" }); - const data = parseResult(result) as { fileRiskMetrics: unknown[] }; - - expect(data).toHaveProperty("fileRiskMetrics"); - expect(Array.isArray(data.fileRiskMetrics)).toBe(true); - }); - - it("fileRiskMetrics entries have blastRadius, complexity, churn", async () => { - const result = await callTool("detect_changes", { scope: "all" }); - const data = parseResult(result) as { - fileRiskMetrics: Array<{ file: string; blastRadius: number; complexity: number; churn: number }>; - }; - - for (const entry of data.fileRiskMetrics) { - expect(entry).toHaveProperty("file"); - expect(entry).toHaveProperty("blastRadius"); - expect(entry).toHaveProperty("complexity"); - expect(entry).toHaveProperty("churn"); - expect(typeof entry.blastRadius).toBe("number"); - expect(typeof entry.complexity).toBe("number"); - expect(typeof entry.churn).toBe("number"); - } - }); -}); - -describe("get_processes tool", () => { - it("returns processes array with totalProcesses", async () => { - const result = await callTool("get_processes", {}); - const data = parseResult(result) as { processes: unknown[]; totalProcesses: number }; - - expect(data).toHaveProperty("processes"); - expect(data).toHaveProperty("totalProcesses"); - expect(Array.isArray(data.processes)).toBe(true); - expect(typeof data.totalProcesses).toBe("number"); - }); - - it("each process has name, entryPoint, steps, depth, modulesTouched", async () => { - const result = await callTool("get_processes", {}); - const data = parseResult(result) as { - processes: Array<{ - name: string; - entryPoint: { file: string; symbol: string }; - steps: unknown[]; - depth: number; - modulesTouched: string[]; - }>; - }; - - for (const p of data.processes) { - expect(p).toHaveProperty("name"); - expect(p).toHaveProperty("entryPoint"); - expect(p.entryPoint).toHaveProperty("file"); - expect(p.entryPoint).toHaveProperty("symbol"); - expect(p).toHaveProperty("steps"); - expect(p).toHaveProperty("depth"); - expect(p).toHaveProperty("modulesTouched"); - } - }); - - it("entryPoint filter works", async () => { - const allResult = await callTool("get_processes", {}); - const allData = parseResult(allResult) as { processes: Array<{ name: string }> }; - - if (allData.processes.length > 0) { - const firstName = allData.processes[0].name; - const filteredResult = await callTool("get_processes", { entryPoint: firstName }); - const filteredData = parseResult(filteredResult) as { processes: unknown[] }; - expect(filteredData.processes.length).toBeLessThanOrEqual(allData.processes.length); - } - }); - - it("limit param restricts results", async () => { - const result = await callTool("get_processes", { limit: 1 }); - const data = parseResult(result) as { processes: unknown[] }; - expect(data.processes.length).toBeLessThanOrEqual(1); - }); - - it("returns empty array when no processes match filter", async () => { - const result = await callTool("get_processes", { entryPoint: "nonexistent_entry_point_xyz" }); - const data = parseResult(result) as { processes: unknown[] }; - expect(result.isError).toBeUndefined(); - expect(data.processes).toEqual([]); - }); -}); - -describe("get_clusters tool", () => { - it("returns clusters array with totalClusters", async () => { - const result = await callTool("get_clusters", {}); - const data = parseResult(result) as { clusters: unknown[]; totalClusters: number }; - - expect(data).toHaveProperty("clusters"); - expect(data).toHaveProperty("totalClusters"); - expect(Array.isArray(data.clusters)).toBe(true); - expect(typeof data.totalClusters).toBe("number"); - }); - - it("each cluster has id, name, files, fileCount, cohesion", async () => { - const result = await callTool("get_clusters", {}); - const data = parseResult(result) as { - clusters: Array<{ - id: string; - name: string; - files: string[]; - fileCount: number; - cohesion: number; - }>; - }; - - for (const c of data.clusters) { - expect(c).toHaveProperty("id"); - expect(c).toHaveProperty("name"); - expect(c).toHaveProperty("files"); - expect(c).toHaveProperty("fileCount"); - expect(c).toHaveProperty("cohesion"); - expect(c.fileCount).toBe(c.files.length); - } - }); - - it("minFiles filter works", async () => { - const allResult = await callTool("get_clusters", {}); - const allData = parseResult(allResult) as { clusters: Array<{ fileCount: number }> }; - - const filteredResult = await callTool("get_clusters", { minFiles: 100 }); - const filteredData = parseResult(filteredResult) as { clusters: unknown[] }; - - expect(filteredData.clusters.length).toBeLessThanOrEqual(allData.clusters.length); - }); -}); - -describe("tool descriptions include disambiguation", () => { - it("all 15 tools are registered", () => { - const tools = getTools(); - const toolNames = Object.keys(tools); - - const expected = [ - "codebase_overview", "file_context", "get_dependents", "find_hotspots", - "get_module_structure", "analyze_forces", "find_dead_exports", "get_groups", - "symbol_context", "search", "detect_changes", "impact_analysis", "rename_symbol", - "get_processes", "get_clusters", - ]; - - for (const name of expected) { - expect(toolNames).toContain(name); - } - }); -}); diff --git a/tests/mcp-tools.test.ts b/tests/mcp-tools.test.ts new file mode 100644 index 0000000..7035532 --- /dev/null +++ b/tests/mcp-tools.test.ts @@ -0,0 +1,381 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getFixturePipeline } from "./helpers/pipeline.js"; +import { registerTools } from "../src/mcp/index.js"; +import { setGraph, setIndexedHead } from "../src/server/graph-store.js"; +import type { CodebaseGraph } from "../src/types/index.js"; + +let client: Client; +let graph: CodebaseGraph; + +beforeAll(async () => { + const pipeline = getFixturePipeline(); + graph = pipeline.codebaseGraph; + setGraph(graph); + setIndexedHead("abc123-test"); + + const server = new McpServer({ name: "test", version: "0.1.0" }); + registerTools(server, graph); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + + client = new Client({ name: "test-client", version: "0.1.0" }); + await client.connect(clientTransport); +}); + +async function callTool(name: string, args: Record = {}): Promise> { + const result = await client.callTool({ name, arguments: args }); + const text = (result.content as Array<{ type: string; text: string }>)[0].text; + return JSON.parse(text) as Record; +} + +describe("Tool 1: codebase_overview", () => { + it("returns totalFiles, modules, topDependedFiles, metrics, nextSteps", async () => { + const r = await callTool("codebase_overview"); + expect(r).toHaveProperty("totalFiles"); + expect(r).toHaveProperty("totalFunctions"); + expect(r).toHaveProperty("totalDependencies"); + expect(r).toHaveProperty("modules"); + expect(r).toHaveProperty("topDependedFiles"); + expect(r).toHaveProperty("metrics"); + expect(r).toHaveProperty("nextSteps"); + expect((r.modules as unknown[]).length).toBeGreaterThan(0); + expect((r.topDependedFiles as unknown[]).length).toBeGreaterThan(0); + const metrics = r.metrics as Record; + expect(metrics).toHaveProperty("avgLOC"); + expect(metrics).toHaveProperty("maxDepth"); + expect(metrics).toHaveProperty("circularDeps"); + }); +}); + +describe("Tool 2: file_context", () => { + it("returns file details for a valid file", async () => { + const files = graph.nodes.filter((n) => n.type === "file"); + const filePath = files[0].id; + const r = await callTool("file_context", { filePath }); + expect(r).toHaveProperty("path", filePath); + expect(r).toHaveProperty("exports"); + expect(r).toHaveProperty("imports"); + expect(r).toHaveProperty("dependents"); + expect(r).toHaveProperty("metrics"); + expect(r).toHaveProperty("nextSteps"); + const m = r.metrics as Record; + expect(m).toHaveProperty("pageRank"); + expect(m).toHaveProperty("fanIn"); + expect(m).toHaveProperty("churn"); + expect(m).toHaveProperty("blastRadius"); + }); + + it("returns error for unknown file", async () => { + const result = await client.callTool({ name: "file_context", arguments: { filePath: "nonexistent.ts" } }); + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0].text; + expect(text).toContain("File not found"); + }); +}); + +describe("Tool 3: get_dependents", () => { + it("returns dependents for a file with importers", async () => { + const fileWithDependents = [...graph.fileMetrics.entries()] + .find(([, m]) => m.fanIn > 0)?.[0]; + if (!fileWithDependents) return; + + const r = await callTool("get_dependents", { filePath: fileWithDependents }); + expect(r).toHaveProperty("directDependents"); + expect(r).toHaveProperty("transitiveDependents"); + expect(r).toHaveProperty("totalAffected"); + expect(r).toHaveProperty("riskLevel"); + expect(r).toHaveProperty("nextSteps"); + expect((r.directDependents as unknown[]).length).toBeGreaterThan(0); + }); + + it("returns error for unknown file", async () => { + const result = await client.callTool({ name: "get_dependents", arguments: { filePath: "nonexistent.ts" } }); + expect(result.isError).toBe(true); + }); +}); + +describe("Tool 4: find_hotspots", () => { + const metrics = [ + "coupling", "pagerank", "fan_in", "fan_out", "betweenness", + "tension", "churn", "complexity", "blast_radius", "coverage", + ] as const; + + for (const metric of metrics) { + it(`ranks files by ${metric}`, async () => { + const r = await callTool("find_hotspots", { metric, limit: 3 }); + expect(r).toHaveProperty("metric", metric); + expect(r).toHaveProperty("hotspots"); + expect(r).toHaveProperty("summary"); + expect(r).toHaveProperty("nextSteps"); + const hotspots = r.hotspots as Array<{ path: string; score: number; reason: string }>; + expect(hotspots.length).toBeGreaterThan(0); + expect(hotspots.length).toBeLessThanOrEqual(3); + expect(hotspots[0]).toHaveProperty("path"); + expect(hotspots[0]).toHaveProperty("score"); + expect(hotspots[0]).toHaveProperty("reason"); + }); + } + + it("ranks modules by escape_velocity", async () => { + const r = await callTool("find_hotspots", { metric: "escape_velocity" }); + expect(r).toHaveProperty("metric", "escape_velocity"); + const hotspots = r.hotspots as unknown[]; + expect(hotspots.length).toBeGreaterThan(0); + }); +}); + +describe("Tool 5: get_module_structure", () => { + it("returns modules with cross-deps and circular deps", async () => { + const r = await callTool("get_module_structure"); + expect(r).toHaveProperty("modules"); + expect(r).toHaveProperty("crossModuleDeps"); + expect(r).toHaveProperty("circularDeps"); + expect(r).toHaveProperty("nextSteps"); + const modules = r.modules as Array>; + expect(modules.length).toBeGreaterThan(0); + expect(modules[0]).toHaveProperty("path"); + expect(modules[0]).toHaveProperty("cohesion"); + expect(modules[0]).toHaveProperty("escapeVelocity"); + }); +}); + +describe("Tool 6: analyze_forces", () => { + it("returns cohesion, tension, bridges, extraction candidates", async () => { + const r = await callTool("analyze_forces"); + expect(r).toHaveProperty("moduleCohesion"); + expect(r).toHaveProperty("tensionFiles"); + expect(r).toHaveProperty("bridgeFiles"); + expect(r).toHaveProperty("extractionCandidates"); + expect(r).toHaveProperty("summary"); + expect(r).toHaveProperty("nextSteps"); + const cohesion = r.moduleCohesion as Array>; + expect(cohesion.length).toBeGreaterThan(0); + expect(cohesion[0]).toHaveProperty("verdict"); + }); + + it("respects custom thresholds", async () => { + const r = await callTool("analyze_forces", { + cohesionThreshold: 0.9, + tensionThreshold: 0.0, + escapeThreshold: 0.0, + }); + const cohesion = r.moduleCohesion as Array<{ verdict: string }>; + expect(cohesion.some((m) => m.verdict !== "COHESIVE")).toBe(true); + }); +}); + +describe("Tool 7: find_dead_exports", () => { + it("returns dead exports across codebase", async () => { + const r = await callTool("find_dead_exports"); + expect(r).toHaveProperty("totalDeadExports"); + expect(r).toHaveProperty("files"); + expect(r).toHaveProperty("summary"); + expect(r).toHaveProperty("nextSteps"); + }); + + it("filters by module", async () => { + const moduleName = [...graph.moduleMetrics.keys()][0]; + const r = await callTool("find_dead_exports", { module: moduleName }); + expect(r).toHaveProperty("files"); + }); +}); + +describe("Tool 8: get_groups", () => { + it("returns ranked directory groups", async () => { + const r = await callTool("get_groups"); + expect(r).toHaveProperty("groups"); + expect(r).toHaveProperty("nextSteps"); + const groups = r.groups as Array>; + expect(groups.length).toBeGreaterThan(0); + expect(groups[0]).toHaveProperty("rank"); + expect(groups[0]).toHaveProperty("name"); + expect(groups[0]).toHaveProperty("files"); + expect(groups[0]).toHaveProperty("loc"); + expect(groups[0]).toHaveProperty("importance"); + expect(groups[0]).toHaveProperty("coupling"); + }); +}); + +describe("Tool 9: symbol_context", () => { + it("returns callers, callees, metrics for a known symbol", async () => { + const symbolName = [...graph.symbolMetrics.values()][0].name; + const r = await callTool("symbol_context", { name: symbolName }); + expect(r).toHaveProperty("name", symbolName); + expect(r).toHaveProperty("file"); + expect(r).toHaveProperty("type"); + expect(r).toHaveProperty("fanIn"); + expect(r).toHaveProperty("fanOut"); + expect(r).toHaveProperty("pageRank"); + expect(r).toHaveProperty("betweenness"); + expect(r).toHaveProperty("callers"); + expect(r).toHaveProperty("callees"); + expect(r).toHaveProperty("nextSteps"); + }); + + it("returns error for unknown symbol", async () => { + const result = await client.callTool({ name: "symbol_context", arguments: { name: "nonexistent_xyz_123" } }); + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0].text; + expect(text).toContain("Symbol not found"); + }); +}); + +describe("Tool 10: search", () => { + it("returns ranked results for a valid query", async () => { + const r = await callTool("search", { query: "auth" }); + expect(r).toHaveProperty("query", "auth"); + expect(r).toHaveProperty("results"); + expect(r).toHaveProperty("nextSteps"); + const results = r.results as Array>; + expect(results.length).toBeGreaterThan(0); + expect(results[0]).toHaveProperty("file"); + expect(results[0]).toHaveProperty("score"); + expect(results[0]).toHaveProperty("symbols"); + }); + + it("returns suggestions for no-match query", async () => { + const r = await callTool("search", { query: "zzzznonexistent_xyz" }); + expect(r).toHaveProperty("results"); + expect((r.results as unknown[]).length).toBe(0); + expect(r).toHaveProperty("suggestions"); + }); +}); + +describe("Tool 11: detect_changes", () => { + it("handles git not available gracefully", async () => { + const r = await callTool("detect_changes"); + expect(r).toHaveProperty("scope"); + }); +}); + +describe("Tool 12: impact_analysis", () => { + it("returns depth-grouped impact for a known symbol", async () => { + const r = await callTool("impact_analysis", { symbol: "UserService.getUserById" }); + expect(r).toHaveProperty("symbol"); + expect(r).toHaveProperty("levels"); + expect(r).toHaveProperty("totalAffected"); + expect(r).toHaveProperty("nextSteps"); + }); + + it("returns empty levels for unknown symbol", async () => { + const r = await callTool("impact_analysis", { symbol: "nonexistent_xyz_123" }); + expect(r).toHaveProperty("totalAffected", 0); + }); +}); + +describe("Tool 13: rename_symbol", () => { + it("returns references for a dry-run rename", async () => { + const r = await callTool("rename_symbol", { oldName: "getUserById", newName: "findUserById" }); + expect(r).toHaveProperty("dryRun", true); + expect(r).toHaveProperty("references"); + expect(r).toHaveProperty("nextSteps"); + }); +}); + +describe("Tool 14: get_processes", () => { + it("returns execution flow traces", async () => { + const r = await callTool("get_processes"); + expect(r).toHaveProperty("processes"); + expect(r).toHaveProperty("totalProcesses"); + expect(r).toHaveProperty("nextSteps"); + const procs = r.processes as unknown[]; + expect(procs.length).toBeGreaterThan(0); + }); + + it("filters by entry point", async () => { + const allResult = await callTool("get_processes"); + const procs = allResult.processes as Array<{ name: string }>; + if (procs.length === 0) return; + + const firstName = procs[0].name; + const r = await callTool("get_processes", { entryPoint: firstName }); + const filtered = r.processes as unknown[]; + expect(filtered.length).toBeLessThanOrEqual(procs.length); + }); + + it("respects limit", async () => { + const r = await callTool("get_processes", { limit: 1 }); + const procs = r.processes as unknown[]; + expect(procs.length).toBeLessThanOrEqual(1); + }); +}); + +describe("Tool 15: get_clusters", () => { + it("returns community-detected clusters", async () => { + const r = await callTool("get_clusters"); + expect(r).toHaveProperty("clusters"); + expect(r).toHaveProperty("totalClusters"); + expect(r).toHaveProperty("nextSteps"); + const clusters = r.clusters as Array>; + expect(clusters.length).toBeGreaterThan(0); + expect(clusters[0]).toHaveProperty("id"); + expect(clusters[0]).toHaveProperty("name"); + expect(clusters[0]).toHaveProperty("files"); + expect(clusters[0]).toHaveProperty("cohesion"); + }); + + it("filters by minFiles", async () => { + const r = await callTool("get_clusters", { minFiles: 100 }); + const clusters = r.clusters as unknown[]; + expect(clusters.length).toBe(0); + }); +}); + +describe("MCP Prompts", () => { + it("detect_impact prompt is registered", async () => { + const prompts = await client.listPrompts(); + const names = prompts.prompts.map((p) => p.name); + expect(names).toContain("detect_impact"); + expect(names).toContain("generate_map"); + }); + + it("detect_impact returns prompt messages", async () => { + const result = await client.getPrompt({ name: "detect_impact", arguments: { symbol: "getUserById" } }); + expect(result.messages.length).toBeGreaterThan(0); + const text = result.messages[0].content as { type: string; text: string }; + expect(text.text).toContain("getUserById"); + }); + + it("generate_map returns prompt messages", async () => { + const result = await client.getPrompt({ name: "generate_map", arguments: {} }); + expect(result.messages.length).toBeGreaterThan(0); + }); +}); + +describe("MCP Resources", () => { + it("lists clusters, processes, and setup resources", async () => { + const resources = await client.listResources(); + const uris = resources.resources.map((r) => r.uri); + expect(uris).toContain("codebase://clusters"); + expect(uris).toContain("codebase://processes"); + expect(uris).toContain("codebase://setup"); + }); + + it("reads clusters resource", async () => { + const result = await client.readResource({ uri: "codebase://clusters" }); + const text = (result.contents[0] as { text: string }).text; + const clusters = JSON.parse(text) as unknown[]; + expect(clusters.length).toBeGreaterThan(0); + }); + + it("reads processes resource", async () => { + const result = await client.readResource({ uri: "codebase://processes" }); + const text = (result.contents[0] as { text: string }).text; + const processes = JSON.parse(text) as unknown[]; + expect(processes.length).toBeGreaterThan(0); + }); + + it("reads setup resource with indexedHead", async () => { + const result = await client.readResource({ uri: "codebase://setup" }); + const text = (result.contents[0] as { text: string }).text; + const setup = JSON.parse(text) as Record; + expect(setup).toHaveProperty("project", "codebase-intelligence"); + expect(setup).toHaveProperty("indexedHead", "abc123-test"); + expect(setup).toHaveProperty("availableTools"); + expect((setup.availableTools as string[]).length).toBe(15); + }); +}); diff --git a/tests/phase1-mcp-api.test.ts b/tests/phase1-mcp-api.test.ts index 6271725..bc806b4 100644 --- a/tests/phase1-mcp-api.test.ts +++ b/tests/phase1-mcp-api.test.ts @@ -4,7 +4,7 @@ import { setGraph } from "../src/server/graph-store.js"; beforeAll(() => { const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); + setGraph(codebaseGraph); }); describe("1.3 — analyze_forces responds to threshold params", () => { @@ -18,22 +18,3 @@ describe("1.3 — analyze_forces responds to threshold params", () => { describe("1.8 — symbol_context MCP tool", () => { it.todo("calling with 'AuthService' returns callers, callees, metrics, nextSteps"); }); - -describe("1.9 — GET /api/symbols/:name route", () => { - it("returns symbol data for AuthService", async () => { - const { GET } = await import("../app/api/symbols/[name]/route.js"); - expect(GET).toBeDefined(); - - const request = new Request("http://localhost/api/symbols/AuthService", { - method: "GET", - }); - const response = await GET(request, { params: Promise.resolve({ name: "AuthService" }) }); - expect(response.status).toBe(200); - - const data = (await response.json()) as { name: string; callers: unknown[]; callees: unknown[]; nextSteps: string[] }; - expect(data.name).toBe("AuthService"); - expect(data.callers).toBeDefined(); - expect(data.callees).toBeDefined(); - expect(data.nextSteps).toBeDefined(); - }); -}); diff --git a/tests/phase2-red.test.ts b/tests/phase2-red.test.ts index 9ef6d2f..1702f18 100644 --- a/tests/phase2-red.test.ts +++ b/tests/phase2-red.test.ts @@ -4,7 +4,7 @@ import { setGraph } from "../src/server/graph-store.js"; beforeAll(() => { const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); + setGraph(codebaseGraph); }); describe("2.1 — BM25 search ranks correctly", () => { @@ -74,40 +74,6 @@ describe("2.3 — search MCP tool", () => { it.todo("search tool returns file-grouped results with symbol locations and nextSteps"); }); -describe("2.5 — GET /api/search route", () => { - it("returns search results for query", async () => { - const { GET } = await import("../app/api/search/route.js"); - expect(GET).toBeDefined(); - - const request = new Request("http://localhost/api/search?q=auth"); - const response = await GET(request); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - query: string; - results: Array<{ file: string; symbols: unknown[] }>; - }; - expect(data.query).toBe("auth"); - expect(data.results.length).toBeGreaterThan(0); - expect(data.results[0]).toHaveProperty("file"); - expect(data.results[0]).toHaveProperty("symbols"); - }); - - it("returns empty results with suggestions for no-match query", async () => { - const { GET } = await import("../app/api/search/route.js"); - const request = new Request("http://localhost/api/search?q=nonexistent_xyz_123"); - const response = await GET(request); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - results: unknown[]; - suggestions: string[]; - }; - expect(data.results).toHaveLength(0); - expect(data.suggestions).toBeDefined(); - }); -}); - describe("2.6 — existing tools include nextSteps", () => { it("codebase_overview MCP handler includes nextSteps in response", async () => { const { getHints } = await import("../src/mcp/hints.js"); diff --git a/tests/phase2-transport.test.ts b/tests/phase2-transport.test.ts deleted file mode 100644 index 60f611b..0000000 --- a/tests/phase2-transport.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import { getFixturePipeline } from "./helpers/pipeline.js"; -import { setGraph } from "../src/server/graph-store.js"; -import type { Server } from "node:http"; - -let httpServer: Server | undefined; -const TEST_PORT = 9876; - -beforeAll(() => { - const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); -}); - -afterAll(async () => { - if (httpServer) { - await new Promise((resolve) => { - httpServer?.close(() => resolve()); - }); - } -}); - -describe("2.4 — HTTP MCP transport", () => { - it("HTTP transport serves MCP tools accessible via client", async () => { - const { createHttpMcpServer } = await import("../src/mcp/transport.js"); - const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); - const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js"); - const { codebaseGraph } = getFixturePipeline(); - - httpServer = await createHttpMcpServer(codebaseGraph, TEST_PORT); - - const client = new Client({ name: "test-client", version: "1.0.0" }); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${TEST_PORT}/mcp`), - ); - - await client.connect(transport); - - const { tools } = await client.listTools(); - expect(tools.length).toBeGreaterThan(0); - - const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("search"); - expect(toolNames).toContain("codebase_overview"); - expect(toolNames).toContain("symbol_context"); - - await client.close(); - }); -}); diff --git a/tests/phase3-red.test.ts b/tests/phase3-red.test.ts index ac46a9a..fabb6e8 100644 --- a/tests/phase3-red.test.ts +++ b/tests/phase3-red.test.ts @@ -7,7 +7,7 @@ import os from "os"; beforeAll(() => { const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); + setGraph(codebaseGraph); }); describe("3.1 — persistence round-trip", () => { @@ -36,26 +36,6 @@ describe("3.1 — persistence round-trip", () => { }); }); -describe("3.2 — staleness detection", () => { - it("detects stale index when HEAD differs from indexed commit", async () => { - const { getStaleness, setIndexedHead } = await import("../src/server/graph-store.js"); - - setIndexedHead("old-hash-123"); - const staleness = getStaleness("new-hash-456"); - - expect(staleness.stale).toBe(true); - }); - - it("reports not stale when HEAD matches indexed commit", async () => { - const { getStaleness, setIndexedHead } = await import("../src/server/graph-store.js"); - - setIndexedHead("same-hash"); - const staleness = getStaleness("same-hash"); - - expect(staleness.stale).toBe(false); - }); -}); - describe("3.3 — entry point detection", () => { it("routes.ts and middleware.ts are identified as entry points", async () => { const { detectEntryPoints } = await import("../src/process/index.js"); @@ -183,58 +163,3 @@ describe("3.8 — detect_changes without git", () => { it.todo("detect_changes returns clear error when git unavailable"); }); -describe("3.9 — MCP resources", () => { - it("resources are registered on server", async () => { - const { createHttpMcpServer } = await import("../src/mcp/transport.js"); - const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); - const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js"); - const { codebaseGraph } = getFixturePipeline(); - - const port = 9878; - const httpServer = await createHttpMcpServer(codebaseGraph, port); - - try { - const client = new Client({ name: "test-client", version: "1.0.0" }); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp`), - ); - await client.connect(transport); - - const { resources } = await client.listResources(); - expect(resources.length).toBeGreaterThanOrEqual(3); - - const resourceUris = resources.map((r) => r.uri); - expect(resourceUris).toContain("codebase://clusters"); - expect(resourceUris).toContain("codebase://processes"); - expect(resourceUris).toContain("codebase://setup"); - - await client.close(); - } finally { - await new Promise((resolve) => { - httpServer.close(() => { resolve(); }); - }); - } - }); -}); - -describe("3.10 — GET /api/changes", () => { - it.todo("GET /api/changes?scope=staged returns changed symbols"); -}); - -describe("3.18 — GET /api/processes", () => { - it("returns process list from analyzer", async () => { - const { GET } = await import("../app/api/processes/route.js"); - expect(GET).toBeDefined(); - - const response = GET(); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - processes: Array<{ name: string; steps: unknown[] }>; - }; - expect(data.processes).toBeDefined(); - expect(data.processes.length).toBeGreaterThanOrEqual(3); - expect(data.processes[0]).toHaveProperty("name"); - expect(data.processes[0]).toHaveProperty("steps"); - }); -}); diff --git a/tests/phase4-red.test.ts b/tests/phase4-red.test.ts index afe5aae..2412c24 100644 --- a/tests/phase4-red.test.ts +++ b/tests/phase4-red.test.ts @@ -4,7 +4,7 @@ import { setGraph } from "../src/server/graph-store.js"; beforeAll(() => { const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); + setGraph(codebaseGraph); }); describe("4.1 — impact_analysis depth-grouped results", () => { @@ -97,64 +97,3 @@ describe("4.6 — get_dependents deprecation notice", () => { }); }); -describe("4.7 — MCP prompts", () => { - it("detect_impact prompt is registered", async () => { - const { createHttpMcpServer } = await import("../src/mcp/transport.js"); - const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); - const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js"); - const { codebaseGraph } = getFixturePipeline(); - - const port = 9879; - const httpServer = await createHttpMcpServer(codebaseGraph, port); - - try { - const client = new Client({ name: "test-client", version: "1.0.0" }); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp`), - ); - await client.connect(transport); - - const { prompts } = await client.listPrompts(); - const promptNames = prompts.map((p) => p.name); - expect(promptNames).toContain("detect_impact"); - expect(promptNames).toContain("generate_map"); - - await client.close(); - } finally { - await new Promise((resolve) => { - httpServer.close(() => { resolve(); }); - }); - } - }); -}); - -describe("4.8 — impact_analysis MCP tool", () => { - it("impact_analysis tool is registered and returns results", async () => { - const { createHttpMcpServer } = await import("../src/mcp/transport.js"); - const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); - const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js"); - const { codebaseGraph } = getFixturePipeline(); - - const port = 9880; - const httpServer = await createHttpMcpServer(codebaseGraph, port); - - try { - const client = new Client({ name: "test-client", version: "1.0.0" }); - const transport = new StreamableHTTPClientTransport( - new URL(`http://127.0.0.1:${port}/mcp`), - ); - await client.connect(transport); - - const { tools } = await client.listTools(); - const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("impact_analysis"); - expect(toolNames).toContain("rename_symbol"); - - await client.close(); - } finally { - await new Promise((resolve) => { - httpServer.close(() => { resolve(); }); - }); - } - }); -}); diff --git a/tests/phase5-red.test.ts b/tests/phase5-red.test.ts deleted file mode 100644 index bea9fc4..0000000 --- a/tests/phase5-red.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { describe, it, expect, beforeAll } from "vitest"; -import { getFixturePipeline } from "./helpers/pipeline.js"; -import { setGraph, setIndexedHead } from "../src/server/graph-store.js"; - -beforeAll(() => { - const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); - setIndexedHead("abc123"); -}); - -describe("5.1 — file tree renders from GET /api/graph", () => { - it("graph response nodes have path and module for directory grouping", async () => { - const { GET } = await import("../app/api/graph/route.js"); - const response = GET(); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - nodes: Array<{ id: string; path: string; module: string; type: string }>; - }; - - const fileNodes = data.nodes.filter((n) => n.type === "file"); - expect(fileNodes.length).toBeGreaterThan(0); - - for (const node of fileNodes) { - expect(node).toHaveProperty("path"); - expect(node).toHaveProperty("module"); - expect(node.path.length).toBeGreaterThan(0); - } - - const modules = [...new Set(fileNodes.map((n) => n.module))]; - expect(modules.length).toBeGreaterThanOrEqual(2); - }); -}); - -describe("5.2 — search bar calls GET /api/search", () => { - it("search returns ranked results with file and score", async () => { - const { GET } = await import("../app/api/search/route.js"); - const request = new Request("http://localhost/api/search?q=auth"); - const response = GET(request); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - query: string; - results: Array<{ file: string; score: number; symbols: Array<{ name: string }> }>; - }; - - expect(data.query).toBe("auth"); - expect(data.results.length).toBeGreaterThan(0); - expect(data.results[0]).toHaveProperty("file"); - expect(data.results[0]).toHaveProperty("score"); - }); -}); - -describe("5.3 — symbol disambiguation", () => { - it("GET /api/symbols/:name resolves re-exports to source definition", async () => { - const { GET } = await import("../app/api/symbols/[name]/route.js"); - const request = new Request("http://localhost/api/symbols/AuthService"); - const response = await GET(request, { params: Promise.resolve({ name: "AuthService" }) }); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - disambiguation?: Array<{ name: string; file: string; symbolId: string }>; - name?: string; - file?: string; - }; - - expect(data.disambiguation).toBeUndefined(); - expect(data.name).toBe("AuthService"); - expect(data.file).toBe("auth/auth-service.ts"); - }); - - it("GET /api/symbols/:name returns disambiguation when multiple source files", async () => { - const { GET } = await import("../app/api/symbols/[name]/route.js"); - const request = new Request("http://localhost/api/symbols/getUserById"); - const response = await GET(request, { params: Promise.resolve({ name: "getUserById" }) }); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - name?: string; - file?: string; - }; - - expect(data.name).toBe("getUserById"); - expect(data.file).toBe("users/user-repository.ts"); - }); -}); - -describe("5.4 — staleness banner from GET /api/meta", () => { - it("meta endpoint includes staleness info", async () => { - const { GET } = await import("../app/api/meta/route.js"); - const response = GET(); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - projectName: string; - staleness: { stale: boolean; indexedHash: string }; - }; - - expect(data.projectName).toBeDefined(); - expect(data.staleness).toBeDefined(); - expect(data.staleness).toHaveProperty("stale"); - expect(data.staleness).toHaveProperty("indexedHash"); - expect(data.staleness.indexedHash).toBe("abc123"); - }); -}); diff --git a/tests/phase6-red.test.ts b/tests/phase6-red.test.ts index 6b99788..fa9fe43 100644 --- a/tests/phase6-red.test.ts +++ b/tests/phase6-red.test.ts @@ -7,7 +7,7 @@ import { setGraph } from "../src/server/graph-store.js"; beforeAll(() => { const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); + setGraph(codebaseGraph); }); describe("6.1 — CLI persistence commands", () => { diff --git a/tests/phase7-red.test.ts b/tests/phase7-red.test.ts deleted file mode 100644 index c71b420..0000000 --- a/tests/phase7-red.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { describe, it, expect, beforeAll } from "vitest"; -import { getFixturePipeline } from "./helpers/pipeline.js"; -import { setGraph } from "../src/server/graph-store.js"; - -beforeAll(() => { - const { codebaseGraph } = getFixturePipeline(); - setGraph(codebaseGraph, "fixture-test"); -}); - -describe("7A.1 — GET /api/symbol-graph", () => { - it("returns symbolNodes[], callEdges[], symbolMetrics[] with correct types (AC-1)", async () => { - const { GET } = await import("../app/api/symbol-graph/route.js"); - const response = GET(); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - symbolNodes: Array<{ - id: string; - name: string; - type: string; - file: string; - loc: number; - pageRank: number; - betweenness: number; - fanIn: number; - fanOut: number; - }>; - callEdges: Array<{ - source: string; - target: string; - callerSymbol: string; - calleeSymbol: string; - confidence: string; - }>; - symbolMetrics: Array<{ - symbolId: string; - name: string; - pageRank: number; - betweenness: number; - fanIn: number; - fanOut: number; - }>; - }; - - expect(Array.isArray(data.symbolNodes)).toBe(true); - expect(Array.isArray(data.callEdges)).toBe(true); - expect(Array.isArray(data.symbolMetrics)).toBe(true); - - expect(data.symbolNodes.length).toBeGreaterThan(0); - expect(data.callEdges.length).toBeGreaterThan(0); - expect(data.symbolMetrics.length).toBeGreaterThan(0); - - const firstNode = data.symbolNodes[0]; - expect(firstNode).toHaveProperty("id"); - expect(firstNode).toHaveProperty("name"); - expect(firstNode).toHaveProperty("type"); - expect(firstNode).toHaveProperty("file"); - expect(firstNode).toHaveProperty("loc"); - expect(firstNode).toHaveProperty("pageRank"); - expect(firstNode).toHaveProperty("betweenness"); - expect(firstNode).toHaveProperty("fanIn"); - expect(firstNode).toHaveProperty("fanOut"); - - const firstEdge = data.callEdges[0]; - expect(firstEdge).toHaveProperty("source"); - expect(firstEdge).toHaveProperty("target"); - expect(firstEdge).toHaveProperty("callerSymbol"); - expect(firstEdge).toHaveProperty("calleeSymbol"); - expect(firstEdge).toHaveProperty("confidence"); - expect(["type-resolved", "text-inferred"]).toContain(firstEdge.confidence); - }); - - it("returns 200 with empty arrays when no symbols exist (AC-E1)", async () => { - const { getGraph } = await import("../src/server/graph-store.js"); - const graph = getGraph(); - - const origNodes = graph.symbolNodes; - const origEdges = graph.callEdges; - const origMetrics = graph.symbolMetrics; - - graph.symbolNodes = []; - graph.callEdges = []; - graph.symbolMetrics = new Map(); - - try { - const { GET } = await import("../app/api/symbol-graph/route.js"); - const response = GET(); - expect(response.status).toBe(200); - - const data = (await response.json()) as { - symbolNodes: unknown[]; - callEdges: unknown[]; - symbolMetrics: unknown[]; - }; - - expect(data.symbolNodes).toEqual([]); - expect(data.callEdges).toEqual([]); - expect(data.symbolMetrics).toEqual([]); - } finally { - graph.symbolNodes = origNodes; - graph.callEdges = origEdges; - graph.symbolMetrics = origMetrics; - } - }); - - it("symbol nodes include pageRank-based size data", async () => { - const { GET } = await import("../app/api/symbol-graph/route.js"); - const response = GET(); - const data = (await response.json()) as { - symbolNodes: Array<{ pageRank: number }>; - }; - - const ranks = data.symbolNodes.map((n) => n.pageRank); - const allNumbers = ranks.every((r) => typeof r === "number" && r >= 0); - expect(allNumbers).toBe(true); - }); -}); - -describe("7A.2 — enriched GET /api/file/[...path]", () => { - it("functions[] include per-export fanIn, fanOut, pageRank (AC-5)", async () => { - const { GET } = await import("../app/api/file/[...path]/route.js"); - const request = new Request("http://localhost/api/file/auth/auth-service.ts"); - const response = await GET(request, { - params: Promise.resolve({ path: ["auth", "auth-service.ts"] }), - }); - - expect(response.status).toBe(200); - const data = (await response.json()) as { - functions: Array<{ - name: string; - loc: number; - fanIn: number; - fanOut: number; - pageRank: number; - }>; - }; - - expect(data.functions.length).toBeGreaterThan(0); - - for (const fn of data.functions) { - expect(fn).toHaveProperty("fanIn"); - expect(fn).toHaveProperty("fanOut"); - expect(fn).toHaveProperty("pageRank"); - expect(typeof fn.fanIn).toBe("number"); - expect(typeof fn.fanOut).toBe("number"); - expect(typeof fn.pageRank).toBe("number"); - } - }); -}); - -describe("7A.3 — enriched GET /api/symbols/[name]", () => { - it("includes loc, type, pageRank, betweenness fields (AC-7)", async () => { - const { GET } = await import("../app/api/symbols/[name]/route.js"); - const request = new Request("http://localhost/api/symbols/AuthService"); - const response = await GET(request, { - params: Promise.resolve({ name: "AuthService" }), - }); - - expect(response.status).toBe(200); - const data = (await response.json()) as { - name: string; - loc: number; - type: string; - pageRank: number; - betweenness: number; - }; - - expect(data).toHaveProperty("loc"); - expect(data).toHaveProperty("type"); - expect(data).toHaveProperty("pageRank"); - expect(data).toHaveProperty("betweenness"); - expect(typeof data.loc).toBe("number"); - expect(typeof data.type).toBe("string"); - expect(typeof data.pageRank).toBe("number"); - expect(typeof data.betweenness).toBe("number"); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 9571886..51be195 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,11 +4,8 @@ "module": "esnext", "moduleResolution": "bundler", "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" + "ES2022" ], - "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, @@ -16,29 +13,13 @@ "resolveJsonModule": true, "isolatedModules": true, "incremental": true, - "noEmit": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": [ - "./*" - ] - }, - "allowJs": true + "noEmit": true }, "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts", - ".next/dev/types/**/*.ts" + "**/*.ts" ], "exclude": [ "node_modules", - "dist", - ".next" + "dist" ] } diff --git a/vitest.config.ts b/vitest.config.ts index 7ab97ee..4390fa5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -10,5 +10,23 @@ export default defineConfig({ test: { globals: true, include: ["src/**/*.test.ts", "tests/**/*.test.ts"], + testTimeout: 15000, + hookTimeout: 15000, + coverage: { + provider: "v8", + include: ["src/**/*.ts"], + exclude: [ + "src/types/**", + "src/cli.ts", + "src/**/*.test.ts", + ], + reporter: ["text", "json-summary"], + thresholds: { + statements: 80, + branches: 70, + functions: 80, + lines: 80, + }, + }, }, });