diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..1defa66
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,20 @@
+# VECTORA local engine configuration. Copy to .env or set in your shell.
+# All values are optional; defaults are shown.
+
+# Loopback address and port of the local engine.
+VECTORA_HOST=127.0.0.1
+VECTORA_PORT=4317
+
+# Where VECTORA keeps its database, imported sources and caches.
+# Default: .vectora-data inside the VECTORA folder (uploads and temp files live here too).
+# VECTORA_DATA_DIR=
+
+# Extra allowed browser origins (comma separated). The engine only accepts
+# same-origin requests plus the Vite dev server by default.
+# VECTORA_ALLOWED_ORIGINS=http://localhost:5173
+
+# Log level: fatal | error | warn | info | debug
+VECTORA_LOG_LEVEL=info
+
+# Python interpreter used for Python source analysis.
+# VECTORA_PYTHON=python
diff --git a/.gitattributes b/.gitattributes
index dfe0770..511187a 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,2 +1,3 @@
-# Auto detect text files and perform LF normalization
-* text=auto
+* text=auto eol=lf
+*.wasm binary
+*.png binary
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..fc75934
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,11 @@
+## What changed
+
+## Why
+
+## How it was checked
+
+- [ ] `npm run typecheck` and `npm run lint`
+- [ ] `npm test`
+- [ ] `npm run build` and `npm run test:e2e` (for interface changes)
+
+## Notes for the reviewer
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9c98a1f
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ name: Type check, lint, test, build
+ # Windows is the platform VECTORA has been developed and tested on.
+ runs-on: windows-latest
+ timeout-minutes: 25
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: false
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ - name: Install dependencies
+ run: npm ci --no-audit --no-fund
+ - name: Type check
+ run: npm run typecheck
+ - name: Lint
+ run: npm run lint
+ - name: Unit and integration tests
+ run: npm test
+ - name: Production build
+ run: npm run build
+ - name: Dependency audit
+ run: npm audit --omit=dev
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6371c1b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+node_modules/
+dist/
+coverage/
+.vectora-data/
+test-results/
+playwright-report/
+*.log
+.env
+.env.*
+!.env.example
+.DS_Store
+Thumbs.db
+*.tsbuildinfo
+.bench-tmp/
+.npm-cache/
+.claude/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..1553fcb
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+cache=.npm-cache
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..191d2a8
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,10 @@
+node_modules
+dist
+coverage
+samples
+package-lock.json
+*.wasm
+.vectora-data
+test-results
+playwright-report
+docs/LICENSES.md
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..e876358
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "semi": true,
+ "singleQuote": true,
+ "trailingComma": "all",
+ "printWidth": 110,
+ "tabWidth": 2
+}
diff --git a/DECISIONS.md b/DECISIONS.md
new file mode 100644
index 0000000..91e7578
--- /dev/null
+++ b/DECISIONS.md
@@ -0,0 +1,92 @@
+# Technical decisions
+
+Decisions made while building VECTORA 0.1, with the reasoning. Newest last.
+
+## D1 — npm workspaces, TypeScript source exports, one `tsconfig`
+
+pnpm and yarn were not installed; npm workspaces need nothing extra. Packages export their TypeScript source (`"exports": "./src/index.ts"`), so there is no per-package build: `tsx` runs the engine in development, Vite and esbuild bundle for release, and one strict `tsconfig.json` type-checks everything.
+
+## D2 — TypeScript 5.9, not 7
+
+The latest `typescript` on the registry is 7.x, the native rewrite, whose programmatic API differs from the Compiler API that the TypeScript adapter depends on (`ts.createProgram`, the type checker, custom module resolution). TypeScript is pinned to `~5.9.3`, the last line with the classic API. Revisit when the 7.x API is stable and documented.
+
+## D3 — `web-tree-sitter` with vendored grammar WASM files
+
+Native Tree-sitter bindings need a compiler toolchain on Windows. `web-tree-sitter` runs everywhere. The widely used `tree-sitter-wasms` package was tested and does **not** load with `web-tree-sitter` 0.27 (ABI mismatch); the WASM files shipped inside the official grammar packages do. Those six files are vendored in `packages/language-adapters/grammars` (MIT, versions listed in `docs/LICENSES.md`) so installation runs no native build scripts.
+
+## D4 — TypeScript Compiler API with the type checker for TypeScript and JavaScript
+
+The brief asks for precise symbol analysis and forbids assuming a call is resolved because a similarly named symbol exists. The type checker gives exactly that: an identifier becomes a relationship only when the checker ties it to a declaration in the repository. Cost: one compiler program per repository, single-threaded (see `docs/PERFORMANCE.md`). Module resolution is implemented by VECTORA against the indexed file list so the compiler never reads `node_modules`.
+
+## D5 — Python analysed by a helper script, not a JavaScript parser
+
+The brief names the Python AST. A small script using the standard-library `ast` module is run with the system interpreter in isolated mode. Trade-off: Python files are not analysed on machines without Python 3; this is detected and shown.
+
+## D6 — Name-based references resolved only with scope evidence
+
+For Python and the Tree-sitter languages there is no type information. The resolver uses, in order: same-file definitions, import bindings, and language visibility rules (same package, included header), and marks the last kind as unverified at confidence 0.7. Ambiguous names and calls on receivers of unknown type are reported as unresolved. This under-reports relationships on purpose; a wrong edge is worse than a missing one.
+
+## D7 — SQLite through `node:sqlite`, no ORM
+
+`better-sqlite3` needs a native binary; `node:sqlite` ships with Node.js 22.13 and later. It is still labelled experimental by Node.js, which is a risk recorded in `KNOWN_ISSUES.md`; the store is one file behind a small class, so swapping the driver is contained. Drizzle was not added: the schema is eight tables with JSON payloads and a handful of statements, and an ORM would add a dependency without removing code.
+
+## D8 — GitHub repositories downloaded as archives; no git commands
+
+Cloning runs git, and git in an untrusted repository can execute configured programs. Downloading the source archive through the REST API (Octokit) avoids executing anything, keeps the token in a header, and reuses the hardened ZIP path. `simple-git` was therefore not needed and was removed. For local folders the branch name is read from `.git/HEAD` as text.
+
+## D9 — Engine serves the web application; loopback only; host and origin checks instead of a token
+
+One process, one origin, so CORS is not needed in production. Because a local service can be reached by any web page in the user's browser, the engine checks `Host` (DNS rebinding) and `Origin`/`Sec-Fetch-Site` (cross-site requests) on every request.
+
+## D10 — Data directory inside the installation folder by default
+
+At the owner's request nothing is written to the system drive. The database, imported sources, uploads and the credential key default to `.vectora-data` in the VECTORA folder; tests and benchmarks use `.bench-tmp`; the npm cache for this project is `.npm-cache`. `VECTORA_DATA_DIR` overrides the location.
+
+## D11 — Sigma.js 3, not 4
+
+Sigma 4 replaced the reducer-based API (node and edge reducers, settings) that the selection, dimming and label behaviour rely on, and its documentation was not sufficient to build against with confidence. Sigma 3.0.3 is the last release of the documented line and works with Graphology 0.26.
+
+## D12 — Visualisation code lives in `apps/web`, not in separate packages
+
+The brief suggests `visualization-2d`, `visualization-3d`, `mindmap` and `ui` packages. The three views are React components tied to the application's store, context and styling; separate packages would add build configuration without a second consumer. The layout engines, which are pure and reusable, are a package (`graph-layout`).
+
+## D13 — Radix primitives and hand-written components instead of generated shadcn/ui components
+
+shadcn/ui is a set of copied components built on Radix. Its default look (rounded cards, soft shadows) is what the design brief rules out, so the components are written directly on Radix (`Dialog`, `Switch`, `Popover`) and `cmdk`, styled with the project's tokens.
+
+## D14 — Custom 3D layout: anchors plus force refinement
+
+A plain 3D force layout does not produce a readable hierarchy. Subsystems are fixed anchors, files are simulated with `d3-force-3d` under a pull towards their anchor, and symbols are placed analytically around their file. This is deterministic, fast for large symbol counts (symbols need no simulation) and gives the centre → regions → contents structure the brief asks for.
+
+## D15 — Labels in 3D as one projected overlay
+
+drei's `Html` creates a React root per label and logged warnings when labels came and went. Labels are plain elements in one overlay, positioned by projecting node positions during rendered frames.
+
+## D16 — Concepts from a fixed vocabulary with evidence
+
+Concept grouping must be based on source evidence and kept apart from facts. Matching path segments, symbol names and imported packages against a fixed vocabulary is transparent: every membership lists exactly why. It is deliberately simple; it will miss domain concepts outside the vocabulary.
+
+## D17 — Question exploration is deterministic
+
+Questions are answered by intent detection and graph queries, so the basic application needs no external service and every statement has a citation. An optional provider adapter (chat-completions format) can rewrite the finished findings as prose; it is off by default and cannot add findings.
+
+## D18 — Credentials encrypted with a key file
+
+AES-256-GCM with a key stored in the data directory. An operating-system keystore would be stronger but needs a native module or platform commands; that is on the roadmap. The limitation is documented in `docs/SECURITY.md`.
+
+## D19 — Not used from the suggested stack
+
+- **chokidar**: no file watching in 0.1; the brief also rules out continuous background work. Re-indexing is on demand.
+- **Comlink**: the two Web Workers have one message type each.
+- **p-limit**: the worker pool bounds concurrency itself.
+- **Drizzle, simple-git**: see D7 and D8.
+- **ELK**: dagre is enough for the tree layout.
+- **Tauri**: plan only, in `docs/ROADMAP.md`.
+
+## D20 — Browser tests use the installed Edge and start the engine without a shell
+
+No browser download is needed (`channel: 'msedge'`). The engine is started from the Playwright global setup with a direct process spawn, because shell-based start-up failed on a machine whose `ComSpec` variable was misconfigured. `npm run build` is a Node script for the same reason.
+
+## D21 — Row-id tables for graph rows
+
+The first schema used `WITHOUT ROWID` tables. Benchmarks showed slow bulk inserts with large JSON rows; ordinary tables with a unique index and multi-row inserts are faster. A migration rebuilds old tables.
diff --git a/IMPLEMENTATION_CHECKLIST.md b/IMPLEMENTATION_CHECKLIST.md
new file mode 100644
index 0000000..70b8772
--- /dev/null
+++ b/IMPLEMENTATION_CHECKLIST.md
@@ -0,0 +1,133 @@
+# Implementation checklist
+
+Status against the master directive. `[x]` done and verified, `[~]` partly done (see note), `[ ]` not done. Notes point to `KNOWN_ISSUES.md` (K) and `DECISIONS.md` (D).
+
+## Phase 0 — Research and architecture
+
+- [x] Inspect workspace and toolchain (empty repository; Node 24.14.1, Python 3.13, no pnpm)
+- [x] Check current versions and compatibility of every dependency (D2, D3, D11)
+- [x] Validate licences (`docs/LICENSES.md`; one package without declared licence, K29)
+- [x] Architecture, design system, decisions documented
+
+## Phase 1 — Foundation
+
+- [x] Monorepo, strict TypeScript, ESLint, Prettier
+- [x] Vite, Tailwind CSS 4, React 19, React Router, TanStack Query, Zustand
+- [x] Typography: Zen Kaku Gothic New, Shippori Mincho, IBM Plex Mono, bundled
+- [x] Light, dark and system themes with centralised tokens
+- [x] Routing and application shell
+- [~] shadcn/ui: Radix primitives used directly with hand-written components (D13)
+
+## Phase 2 — Repository ingestion
+
+- [x] GitHub import: public, private with token, validation, branch selection, subdirectory
+- [x] ZIP import: format validation, nested root detection, structure preview, exclusions
+- [x] Local folder import with folder browser, `.gitignore` and configurable exclusions
+- [x] Import progress with stages
+- [x] File scanning, language detection, manifest and framework detection, entry points
+- [x] Bundled, labelled sample repository
+
+## Phase 3 — Code intelligence
+
+- [x] Tree-sitter (Java, Go, Rust, C, C++, C#)
+- [x] TypeScript Compiler API with type checker (TypeScript, JavaScript)
+- [x] Python `ast`
+- [x] Language-adapter interface: parsing, symbols, relationships, locations, errors, metadata
+- [x] Symbols: classes, functions, methods, interfaces, types, variables, constants, decorators, components, endpoints, models
+- [x] Relationships: IMPORTS, EXPORTS, CALLS, EXTENDS, IMPLEMENTS, USES, INSTANTIATES, ROUTES_TO, READS_FROM, WRITES_TO, DEFINED_IN, BELONGS_TO, CONTAINS, DEPENDS_ON, RELATED_TO
+- [x] Unresolved references reported, never guessed
+- [x] Execution flows: entry points, route handlers, data-access paths, cross-subsystem calls
+- [x] Graph validation, duplicate detection, normalisation, integrity checks
+- [x] Unsupported files do not fail indexing; language coverage displayed
+- [~] Receiver-type inference outside TypeScript (K1, K2); mounted route prefixes (K4)
+
+## Phase 4 — Knowledge graph
+
+- [x] Typed schema: 18 node types, 15 relationship types, evidence, method, confidence
+- [x] Graph engine: adjacency, traversal, shortest path, cycles, impact
+- [x] Persistence in SQLite
+- [x] Community detection (Louvain), subsystems, concepts
+- [x] Incremental indexing with cache and unchanged fast path
+- [x] Search: names, partial matches, paths, qualified names, descriptions, documentation
+
+## Phase 5 — 2D Graph Explorer
+
+- [x] Sigma.js with Graphology, ForceAtlas2 in a worker, no-overlap pass
+- [x] Pan, zoom, fit, minimap, node drag
+- [x] Selection, multi-selection, neighbour highlighting, dimming, inspector
+- [x] Filtering, focus mode, neighbour expansion, community grouping, zoom-dependent labels
+- [x] Search integration
+- [~] Node shapes by type in 2D (glyph, size and label instead; K17)
+
+## Phase 6 — 3D Atlas
+
+- [x] Real 3D scene (Three.js, React Three Fiber, drei)
+- [x] Spatial clusters: repository centre, subsystem regions, files and symbols within, concepts and packages in layers
+- [x] Orbit, pan, zoom, click, double-click, hover, search-to-focus, breadcrumb, reset, focus subsystem, neighbourhood, global view
+- [x] Smooth camera transitions with reduced-motion alternative; exit from focus mode
+- [x] InstancedMesh, BufferGeometry, worker layout, level of detail, label culling, on-demand rendering
+- [x] WebGL context loss handling and 2D fallback
+- [~] Performance verified by measurement in the browser (not measured; K14)
+
+## Phase 7 — Mind Map
+
+- [x] Hierarchy generated from the same graph
+- [x] Expand, collapse, reorganise, search, focus, navigate to graph nodes, inspect source
+- [x] Filter by relationship (overlay)
+- [x] Export SVG and PNG
+
+## Phase 8 — Exploration and documentation
+
+- [x] Inspector: all twelve items, CodeMirror 6 preview, copy actions, focus, expand, resize, collapse
+- [x] Command palette: Ctrl/Cmd+K, Escape, Enter
+- [x] Question exploration with file and line evidence; says so when it cannot verify
+- [x] Provider-independent adapter for optional narrative, off by default (not exercised live; K32)
+- [x] About page: philosophy, how it works, languages, privacy, credits, version, licence, changelog and documentation references
+- [x] Settings: appearance, graph, performance, repository, privacy — persisted and effective
+- [x] Exports: JSON, GraphML, SVG, PNG, Markdown analysis, architecture document; layouts and sessions saved and restored
+- [x] No source text in exports unless requested
+- [x] User-facing copy free of excluded terminology (checked by a test)
+
+## Phase 9 — Testing
+
+- [x] Unit tests: scanning, ignore handling, extraction, resolution, validation, clustering, search, persistence, export
+- [x] Integration tests: ZIP, GitHub (stand-in API), local import, full indexing, graph generation, incremental indexing, workspace persistence
+- [x] Browser tests: landing, import, navigation, graph selection, inspection, mind map, 3D camera, search, settings, export — at desktop, tablet and mobile widths
+- [x] Accessibility checks: keyboard, names and labels, focus visibility, contrast, reduced motion, landmarks, dialogs
+- [x] Security tests: traversal, malicious names, oversized files, invalid archives, credential leakage
+- [x] Benchmarks: 1,000 / 5,000 / 10,000 files; 50,000 symbols and 100,000 relationships
+- [ ] Rendering stress tests, memory-leak checks, WebGL cleanup verification (K14)
+- [ ] Live GitHub import test (K31)
+
+## Phase 10 — Release
+
+- [x] Production build (`dist/web`, `dist/engine`) verified by the browser tests
+- [x] Environment configuration (`.env.example`)
+- [x] Installation and development instructions
+- [x] Versioning, changelog, licence, dependency licence inventory
+- [x] Security notes, error handling, production logging with redaction
+- [x] Dependency audit clean
+- [~] Desktop packaging: plan written, not built (K33)
+- [x] Screenshots and a recorded tour in the README, captured from the real application
+- [x] Continuous integration workflow (type check, lint, tests, build, audit)
+
+## Definition of done
+
+| # | Requirement | Status |
+|---|---|---|
+| 1 | Open VECTORA | Done |
+| 2 | Import an actual repository | Done (local and ZIP verified by tests; GitHub against a stand-in) |
+| 3 | Index its source files | Done |
+| 4 | Extract symbols and dependencies | Done |
+| 5 | Persistent knowledge graph | Done |
+| 6 | Hierarchical mind map | Done |
+| 7 | Functional 2D graph | Done |
+| 8 | Functional 3D graph | Done |
+| 9 | Select nodes | Done |
+| 10 | Inspect original source | Done |
+| 11 | Trace relationships | Done |
+| 12 | Search the repository | Done |
+| 13 | Navigate between the three views | Done |
+| 14 | Save and restore the workspace | Done |
+| 15 | Export the graph | Done |
+| 16 | Light and dark themes | Done |
diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md
new file mode 100644
index 0000000..e4ae008
--- /dev/null
+++ b/KNOWN_ISSUES.md
@@ -0,0 +1,59 @@
+# Known issues and limitations
+
+Version 0.1.0, 2026-11-12. Each item says what the limitation is and, where there is one, the workaround.
+
+## Analysis
+
+1. **Calls on values of unknown type are not resolved in Python and the tier-2 languages.** `x.method()` is resolved only when `x` is `self`/`this`, an imported module or package, or a class name. Such calls are listed as unresolved rather than guessed. TypeScript and JavaScript are not affected (the type checker is used).
+2. **Inherited members are not followed** for `self.x()` / `this.x()` outside TypeScript: a method defined on a base class is reported as “member not found on enclosing type”.
+3. **Data-access relationships are pattern based** (`READS_FROM`, `WRITES_TO`). They depend on recognising a model and a method name, and are always marked as derived. ORMs that hide models behind generated clients (for example a `prisma.user` accessor) are not recognised.
+4. **Route prefixes from mounted routers are not applied.** `app.use('/api', router)` does not prefix the endpoints of `router`.
+5. **CommonJS exports are not marked as exported.** `module.exports = …` and `exports.x = …` are analysed for calls and imports, but the symbols do not get the `exported` flag or an `EXPORTS` edge.
+6. **Concepts come from a fixed vocabulary.** Domain concepts that are not in it are not found, and a file can match on a name that means something else. Every membership shows its evidence and is marked as derived.
+7. **Vue and Svelte single-file components, and languages other than the nine supported, are indexed as files only.**
+8. **C and C++ preprocessing is not performed.** Code inside conditional compilation is analysed as written; macros that generate declarations are not expanded. `.h` files are treated as C++ when the repository contains C++ sources.
+9. **Nested functions are not separate nodes.** Calls made inside a nested function are attributed to the enclosing top-level function or method.
+10. **Entry points are found by manifest fields and naming conventions**, not by build configuration. Python `if __name__ == "__main__":` blocks are not used as evidence.
+11. **Go standard-library detection is by import path shape** (no dot in the first segment).
+
+## Performance
+
+12. **A change to one TypeScript or JavaScript file costs almost as much as a full index**, because the compiler program is created again (see `docs/PERFORMANCE.md`). Python and tier-2 changes are fast.
+13. **TypeScript analysis uses one thread.**
+14. **No rendering benchmark.** Initial render time, frame times while interacting, memory growth over long sessions and WebGL resource release were not measured. Indexing and graph operations were.
+15. **Repositories above 10,000 files were not benchmarked**, and the 10,000-file run was 70% TypeScript. A much larger all-TypeScript repository may exceed the worker's 3 GB heap limit; analysis of that language then fails with a reported error and the files remain as file nodes. Workaround: import a subdirectory or add ignore patterns.
+16. **Search over 50,000 nodes takes about 56 ms per query** on the test machine: a linear scan, fine for interactive use, not indexed.
+
+## Interface
+
+17. **Node shapes differ by type only in 3D.** In the 2D view types are distinguished by glyph, size and label, not by shape.
+18. **“Open source location” opens the file inside VECTORA**, not in an external editor.
+19. **The 2D canvas is keyboard operable but nodes are not individually exposed to assistive technology.** The Mind Map, the Search page and the inspector lists present the same content as ordinary focusable elements.
+20. **PNG export captures the current view**, not the whole graph. The Mind Map exports the whole map.
+21. **Screenshots and the recorded tour show the sample repository only**, and the user guide has no screenshots. They are regenerated with `npx tsx scripts/screenshots.ts --record`.
+22. **A browser tab that was open during a rebuild may need a reload** to pick up new script files.
+
+## Security
+
+23. **The credential key is stored in the data directory** next to the database. It protects against disclosure of the database alone, not against someone who can read the whole data directory. An operating-system keystore is planned.
+24. **Owner-only file permissions on the key file are not enforced on Windows.**
+25. **The engine has no authentication** because it listens on the loopback interface only. Do not expose its port to a network.
+
+## Platform and dependencies
+
+26. **`node:sqlite` is labelled experimental by Node.js.** Its warning is suppressed at start-up. A change in a future Node.js version could require an update to `packages/graph-engine/src/store.ts`.
+27. **Developed and tested on Windows 11 only**, with Node.js 24.14.1 and Microsoft Edge. macOS, Linux, Firefox and Safari were not tested.
+28. **`jsdom` 31 (test dependency) declares support for Node.js 24.15 or newer**; on 24.14.1 npm prints an engine warning. The tests pass.
+29. **`webgl-constants` (a transitive dependency of the 3D stack) declares no licence in its package metadata.** Flagged in `docs/LICENSES.md` for manual review.
+30. **`typescript` and `web-tree-sitter` are not bundled into the engine**; `node_modules` must be present beside `dist` at run time.
+
+## Not verified
+
+31. **A live GitHub import.** GitHub access is tested against a local stand-in for the API, not against github.com.
+32. **The optional narrative provider against a real service.** The adapter is implemented and off by default; it has not been exercised with a live endpoint.
+33. **Desktop packaging.** Not built; `docs/ROADMAP.md` holds the plan.
+34. **Symbolic-link handling in the scanner on Windows** is covered by a test that skips itself when the account may not create links.
+
+## Environment note for this machine
+
+35. The `ComSpec` environment variable on the development machine points at a MySQL folder instead of `cmd.exe`. Tools that start a shell fail with `spawn … ENOENT`. The project's own scripts avoid shells, but the variable should be corrected in the system settings.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fac4501
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 VECTORA contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PROJECT_STATUS.md b/PROJECT_STATUS.md
new file mode 100644
index 0000000..9b451de
--- /dev/null
+++ b/PROJECT_STATUS.md
@@ -0,0 +1,72 @@
+# Project status
+
+**Version:** 0.1.0
+**Date:** 2026-11-12
+**Branch:** `feat/vectora`
+**State:** Feature complete for the first web release. All automated checks pass. Not yet released; no desktop artifact.
+
+## Summary
+
+VECTORA is a working application: a local engine that imports and indexes repositories into a persistent knowledge graph, and a web application that explores that graph in a 2D graph, a 3D atlas and a mind map with a shared selection, an inspector showing original source, search, question exploration with citations, a dependency explorer, exports and settings.
+
+## Verification on 2026-11-12
+
+| Check | Command | Result |
+|---|---|---|
+| Types | `npm run typecheck` | No errors |
+| Lint | `npm run lint` | No errors |
+| Unit and integration | `npm test` | 79 passed, 0 failed |
+| Coverage | `npm run test:coverage` | 83.6% lines, 80.5% statements, 65.5% branches |
+| Browser journeys | `npm run build` then `npm run test:e2e` | 18 passed, 0 failed (desktop, tablet, mobile) |
+| Production build | `npm run build` | `dist/web`, `dist/engine` produced and exercised by the browser tests |
+| Dependency audit | `npm audit --omit=dev` | 0 vulnerabilities |
+| Benchmarks | `npm run bench` | 1,000 / 5,000 / 10,000 files and a 50,000-symbol graph measured; see `docs/PERFORMANCE.md` |
+
+Environment: Windows 11, Node.js 24.14.1, Python 3.13.2, Microsoft Edge.
+
+## What works
+
+Everything marked done in `IMPLEMENTATION_CHECKLIST.md`, including all sixteen items of the definition of done.
+
+## What is incomplete
+
+| Item | State |
+|---|---|
+| Rendering benchmarks, memory-leak and WebGL cleanup checks | Not done |
+| Live test against github.com | Not done; a stand-in API is used in tests |
+| Optional narrative provider with a real service | Implemented, not exercised |
+| Desktop packaging (Tauri 2) | Plan only |
+| Receiver-type inference outside TypeScript, mounted route prefixes, 2D node shapes | Not done |
+| macOS, Linux, other browsers | Not tested |
+
+Details and workarounds: `KNOWN_ISSUES.md`.
+
+## Release artifacts
+
+Produced by `npm run build` (not committed):
+
+- `dist/web/` — the web application
+- `dist/engine/server.mjs`, `dist/engine/analyze-worker.mjs`, `dist/engine/grammars/`, `dist/engine/python/` — the engine
+
+Run with `npm start`. `node_modules` must be present. There is no installer and no desktop build.
+
+## Next actions
+
+1. Correct the `ComSpec` environment variable on the development machine (see `KNOWN_ISSUES.md`, item 35).
+2. Try a real GitHub import, public and private, and record the result.
+3. Add a browser rendering benchmark (frame times, memory) for the 5,000-file synthetic repository.
+4. Reuse the TypeScript compiler program between runs to make single-file re-indexing fast.
+5. Add screenshots to the user guide.
+6. Test on macOS and Linux, and in Firefox.
+7. Decide on desktop packaging and settle the open question in the Tauri plan.
+8. Merge `feat/vectora` into `main` and tag `v0.1.0`.
+
+## Where things are
+
+| Need | Location |
+|---|---|
+| How to run | `README.md` |
+| How it is built | `docs/ARCHITECTURE.md` |
+| Why it is built that way | `DECISIONS.md` |
+| What is left | `IMPLEMENTATION_CHECKLIST.md`, `KNOWN_ISSUES.md`, `docs/ROADMAP.md` |
+| Test and benchmark results | `docs/TESTING.md`, `docs/PERFORMANCE.md`, `docs/benchmarks/latest.json` |
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d00b747
--- /dev/null
+++ b/README.md
@@ -0,0 +1,292 @@
+
+
+# VECTORA
+
+### Understand what connects.
+
+Turn any software repository into a knowledge graph you can walk through —
+in two dimensions, in three, or as a mind map. Everything runs on your machine.
+
+[](https://github.com/shrenixcodes/VECTORA/actions/workflows/ci.yml)
+
+
+
+
+
+
+
+
+
+
Recorded from the real application on the bundled sample repository. Nothing is staged.
+
+
+
+
+
+## What it does
+
+VECTORA is a codebase intelligence platform that transforms software repositories into structured, interactive knowledge maps. It reveals how components, modules, dependencies and concepts connect, giving developers a new way to understand complex software systems.
+
+Point it at a **GitHub repository**, a **ZIP archive** or a **local folder**. It parses the source, works out what calls what, what imports what and where requests end up, and keeps the line of code that proves each relationship. Then you explore.
+
+
+
+## Three views, one graph
+
+Select something in one view and the other two follow.
+
+
+
+
+
+### 3D Atlas
+
+The repository at the centre, subsystems around it, files and symbols inside their subsystem, concepts above. Orbit, fly to a node, unfold it, return to the whole.
+
+
+
+
+
+
+
+
+
+
+
+
+### Graph Explorer
+
+Pan and zoom a two-dimensional map. Selecting a node lights up what it depends on and what depends on it, labels each relationship, and dims the rest.
+
+
+
+
+
+
+### Mind Map
+
+Subsystems, folders, files and symbols as a tree you can expand, rearrange, search and export as SVG or PNG.
+
+
+
+
+
+
+
+
+
+
+## Every claim comes with evidence
+
+
+
+- **Ask in plain words.** “What calls `verifyToken`?”, “What could be affected if I change this class?”, “Trace `GET /notes`”, “Identify circular dependencies”. Every statement in the answer cites the file and line it comes from.
+- **Open any relationship** in the inspector to see the exact line of source behind it, how it was found, and with what confidence.
+- **Nothing is guessed.** A reference is linked to a definition only when there is scope evidence for it. Everything else is listed as unresolved instead of being drawn.
+- **Facts and interpretation are kept apart.** Groupings such as concepts are marked as derived and drawn with dashed lines.
+
+
+
+## More to look at
+
+
+
+
+
+Overview — architecture summary, languages, entry points, subsystems
+
+
+
+Dependency Explorer — packages, cycles, flows, unresolved references
+
+
+
+
+
+Focus — fly to a symbol; the breadcrumb shows where you are
+
+
+
+Light and dark themes — a quiet, editorial interface
+
+
+
+
+
+
+## Get started in three steps
+
+You need [Node.js](https://nodejs.org) 22.13 or newer. Python 3.8 or newer is needed only if you want Python code analysed.
+
+**1. Get the code and install**
+
+```bash
+git clone https://github.com/shrenixcodes/VECTORA.git
+```
+
+```bash
+cd VECTORA
+```
+
+```bash
+npm install
+```
+
+**2. Build and start**
+
+```bash
+npm run build
+```
+
+```bash
+npm start
+```
+
+**3. Open it**
+
+Go to ****, choose **Explore a repository**, then **Index the sample repository**. In a few seconds you are inside “Orbit Notes”, a small multi-language sample that ships with VECTORA for exactly this purpose.
+
+When you are ready, import your own code from the same screen.
+
+> **Tip.** Press Ctrl + K (Cmd + K on macOS) anywhere in a workspace to search files, symbols, concepts and documentation.
+
+
+
+## Importing your own repository
+
+| Source | How |
+|---|---|
+| **GitHub** | Paste the URL, choose **Check repository**, pick a branch and optionally a subdirectory. For a private repository add an access token in the *Private repository* section — never in the URL. |
+| **ZIP archive** | Drop the file. VECTORA checks it before extracting anything and lets you pick the project root. |
+| **Local folder** | Type a path or choose **Browse**. The folder is read in place; nothing is copied or changed. |
+
+`.gitignore` files are respected. Dependencies are never installed and nothing in the repository is ever run.
+
+
+
+## Features
+
+| | |
+|---|---|
+| **Exploration** | 2D Graph Explorer, 3D Atlas, Mind Map, with shared selection, focus mode, filters and saved layouts |
+| **Inspector** | Original source, incoming and outgoing relationships, evidence, parent module, related concepts |
+| **Search** | Command palette, type filters, documentation search |
+| **Questions** | Callers, dependents, impact of a change, traces, data access paths, cycles, startup sequence |
+| **Dependencies** | External packages, subsystem dependencies, circular imports, execution flows, unresolved references |
+| **Indexing** | Incremental; unchanged files are reused; analysis runs in worker threads |
+| **Exports** | Graph as JSON or GraphML; mind map as SVG or PNG; views as PNG; analysis and architecture documents as Markdown |
+| **Workspace** | Selection, filters, layouts and view are saved and restored automatically |
+| **Accessibility** | Keyboard access, visible focus, reduced-motion support, colour never the only signal |
+
+
+
+## Supported languages
+
+| Language | Tier | How it is analysed |
+|---|:-:|---|
+| TypeScript, JavaScript | 1 | TypeScript Compiler API; references resolved by the type checker |
+| Python | 1 | Standard-library `ast`; imports and scope resolved by the indexer |
+| Java, Go, Rust, C, C++, C# | 2 | Tree-sitter syntax trees; imports and scope resolved by the indexer |
+
+Files in other languages are indexed as files and are searchable, but are not analysed at symbol level. Every repository shows its own language coverage. VECTORA does not claim to support every language.
+
+
+
+## Your code stays on your machine
+
+- Indexing, the graph database and search all run in a local engine. **No source code is uploaded.**
+- The engine listens on the loopback interface only and rejects requests from other origins.
+- Access tokens are stored encrypted, sent only to GitHub in a request header, and never written to logs, URLs or exports.
+- Exports leave out source text unless you ask for it.
+- No telemetry, no account.
+
+Everything VECTORA stores lives in one folder, `.vectora-data` inside the VECTORA folder by default. Set `VECTORA_DATA_DIR` to move it; see [`.env.example`](.env.example). Details are in [docs/SECURITY.md](docs/SECURITY.md).
+
+
+
+## How it works
+
+```
+repository ─► discovery ─► syntax analysis ─► relationship resolution ─► flows ─► organisation ─► knowledge graph
+ │
+ Graph Explorer ◄──┐ ▼
+ 3D Atlas ◄──┼── one graph, one selection ◄── local SQLite
+ Mind Map ◄──┘
+```
+
+```
+apps/
+ local-engine/ Fastify service: imports, indexing jobs, graph API, exports
+ web/ React application: landing, import, workspace, three views
+packages/
+ graph-schema/ Node and relationship types, validation
+ language-adapters/ TypeScript, Python and Tree-sitter adapters behind one interface
+ repository-indexer/ Scanning, resolution, organisation, worker pool
+ graph-engine/ Algorithms, SQLite store, exports, question exploration
+ graph-layout/ 3D and 2D layout engines
+ search/ Search index
+ shared/ Settings schema, API contracts
+```
+
+More in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
+
+
+
+## Development
+
+```bash
+npm run dev
+```
+
+starts the engine and the web application with reloading; open .
+
+| Command | Purpose |
+|---|---|
+| `npm test` | Unit and integration tests |
+| `npm run test:e2e` | Browser journeys against the production build (run `npm run build` first) |
+| `npm run typecheck` / `npm run lint` | Static checks |
+| `npm run bench` | Performance benchmarks |
+| `npx tsx scripts/screenshots.ts --record` | Re-create the screenshots and the tour in this README |
+
+Current state: 79 unit and integration tests and 18 browser journeys pass. On the development machine a 10,000-file synthetic repository indexed in 31 seconds with a warm file cache (54 seconds cold). Results, coverage and what is not yet tested are in [docs/TESTING.md](docs/TESTING.md) and [docs/PERFORMANCE.md](docs/PERFORMANCE.md).
+
+
+
+## Documentation
+
+| | |
+|---|---|
+| [User guide](docs/USER_GUIDE.md) | Importing, exploring, exporting |
+| [Architecture](docs/ARCHITECTURE.md) | Components and how they interact |
+| [Data model](docs/DATA_MODEL.md) | Every node and relationship type |
+| [Indexing pipeline](docs/INDEXING_PIPELINE.md) | Scanning, parsing, resolution, organisation |
+| [Graph engine](docs/GRAPH_ENGINE.md) | Persistence and algorithms |
+| [Visualisation](docs/VISUALIZATION.md) | 2D, 3D and mind map rendering |
+| [Design system](docs/DESIGN_SYSTEM.md) | Typography, colour, spacing, motion |
+| [Security](docs/SECURITY.md) | Threat model and protections |
+| [Performance](docs/PERFORMANCE.md) | Benchmarks and optimisation decisions |
+| [Testing](docs/TESTING.md) | Commands, results, coverage |
+| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common problems and fixes |
+| [Contributing](docs/CONTRIBUTING.md) | Setup and guidelines |
+| [Roadmap](docs/ROADMAP.md) · [Changelog](docs/CHANGELOG.md) · [Licences](docs/LICENSES.md) | |
+
+Project records: [status](PROJECT_STATUS.md), [checklist](IMPLEMENTATION_CHECKLIST.md), [decisions](DECISIONS.md), [known issues](KNOWN_ISSUES.md).
+
+
+
+## Status and limits
+
+Version 0.1.0 is the first release. It has been developed and tested on Windows 11 with Microsoft Edge; macOS, Linux and other browsers are untested. There is no packaged desktop application yet. The full list of limitations is in [KNOWN_ISSUES.md](KNOWN_ISSUES.md).
+
+## License
+
+MIT. See [LICENSE](LICENSE). Third-party licences are listed in [docs/LICENSES.md](docs/LICENSES.md).
+
+
+
+
+
+**VECTORA**
+Understand what connects.
+
+
diff --git a/apps/local-engine/package.json b/apps/local-engine/package.json
new file mode 100644
index 0000000..fc71bc7
--- /dev/null
+++ b/apps/local-engine/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@vectora/local-engine",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "dev": "tsx watch src/main.ts",
+ "start": "tsx src/main.ts"
+ },
+ "dependencies": {
+ "@fastify/cors": "^11.4.1",
+ "@fastify/multipart": "^10.1.8",
+ "@fastify/static": "^10.1.10",
+ "@vectora/graph-engine": "*",
+ "@vectora/graph-schema": "*",
+ "@vectora/language-adapters": "*",
+ "@vectora/repository-indexer": "*",
+ "@vectora/search": "*",
+ "@vectora/shared": "*",
+ "fastify": "^5.12.8",
+ "ignore": "^7.0.14",
+ "octokit": "^5.0.5",
+ "yauzl": "^3.4.0",
+ "zod": "^4.6.7"
+ },
+ "devDependencies": {
+ "@types/yauzl": "^2.10.3",
+ "fflate": "^0.8.3"
+ }
+}
diff --git a/apps/local-engine/src/config.ts b/apps/local-engine/src/config.ts
new file mode 100644
index 0000000..42cae3c
--- /dev/null
+++ b/apps/local-engine/src/config.ts
@@ -0,0 +1,71 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { DEFAULT_ENGINE_PORT } from '@vectora/shared';
+
+export interface EngineConfig {
+ host: string;
+ port: number;
+ /** Database, imported sources, uploads and temporary files all live here. */
+ dataDir: string;
+ /** Browser origins allowed in addition to the engine's own origin. */
+ allowedOrigins: string[];
+ logLevel: string;
+ /** Built web application to serve, when present. */
+ webDist: string | null;
+ sampleDir: string | null;
+ installRoot: string;
+ production: boolean;
+ /** Replaceable network transport (tests supply a stub; nothing else should). */
+ fetch?: typeof fetch;
+}
+
+/** Walks up from this file to the VECTORA installation root. */
+export function findInstallRoot(): string {
+ let dir = path.dirname(fileURLToPath(import.meta.url));
+ for (let i = 0; i < 8; i++) {
+ const pkg = path.join(dir, 'package.json');
+ if (fs.existsSync(pkg)) {
+ try {
+ if ((JSON.parse(fs.readFileSync(pkg, 'utf8')) as { name?: string }).name === 'vectora') return dir;
+ } catch {
+ // keep walking
+ }
+ }
+ const parent = path.dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ return process.cwd();
+}
+
+export function loadConfig(overrides: Partial = {}): EngineConfig {
+ const env = process.env;
+ const installRoot = overrides.installRoot ?? findInstallRoot();
+ const webDist =
+ [path.join(installRoot, 'dist', 'web'), path.join(installRoot, 'apps', 'web', 'dist')].find((d) =>
+ fs.existsSync(path.join(d, 'index.html')),
+ ) ?? null;
+ const sampleDir =
+ [
+ path.join(installRoot, 'samples', 'sample-repo'),
+ path.join(installRoot, 'dist', 'samples', 'sample-repo'),
+ ].find((d) => fs.existsSync(d)) ?? null;
+ const port = Number(env.VECTORA_PORT ?? DEFAULT_ENGINE_PORT);
+ return {
+ host: env.VECTORA_HOST ?? '127.0.0.1',
+ port: Number.isFinite(port) ? port : DEFAULT_ENGINE_PORT,
+ // Kept inside the installation folder by default so nothing is written to the system drive.
+ dataDir: path.resolve(env.VECTORA_DATA_DIR ?? path.join(installRoot, '.vectora-data')),
+ allowedOrigins: (env.VECTORA_ALLOWED_ORIGINS ?? 'http://localhost:5173,http://127.0.0.1:5173')
+ .split(',')
+ .map((s) => s.trim())
+ .filter(Boolean),
+ logLevel: env.VECTORA_LOG_LEVEL ?? 'info',
+ webDist,
+ sampleDir,
+ installRoot,
+ production: env.NODE_ENV === 'production',
+ ...overrides,
+ };
+}
diff --git a/apps/local-engine/src/github.ts b/apps/local-engine/src/github.ts
new file mode 100644
index 0000000..decf9eb
--- /dev/null
+++ b/apps/local-engine/src/github.ts
@@ -0,0 +1,162 @@
+/**
+ * GitHub access through the REST API. Repositories are downloaded as source
+ * archives, so no git commands or repository hooks ever run. Access tokens
+ * travel only in the Authorization header.
+ */
+import fs from 'node:fs';
+import { Octokit } from 'octokit';
+
+export interface GitHubRef {
+ owner: string;
+ repo: string;
+ branch: string | null;
+ subdirectory: string | null;
+}
+
+export class GitHubError extends Error {
+ constructor(
+ message: string,
+ public status: number,
+ ) {
+ super(message);
+ }
+}
+
+const NAME = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,99})$/;
+
+/** Accepts https URLs, `git@github.com:owner/repo.git` and `owner/repo`. */
+export function parseGitHubUrl(input: string): GitHubRef {
+ const raw = input.trim();
+ if (!raw) throw new GitHubError('Enter a GitHub repository URL', 400);
+ if (/^[a-z]+:\/\/[^/\s]*@/i.test(raw))
+ throw new GitHubError('Do not put credentials in the URL. Use the access token field instead.', 400);
+ let rest: string;
+ const ssh = /^git@github\.com:(.+)$/i.exec(raw);
+ if (ssh) rest = ssh[1]!;
+ else if (/^https?:\/\//i.test(raw)) {
+ let url: URL;
+ try {
+ url = new URL(raw);
+ } catch {
+ throw new GitHubError('That is not a valid URL', 400);
+ }
+ if (!/^(www\.)?github\.com$/i.test(url.hostname))
+ throw new GitHubError('Only github.com repositories are supported', 400);
+ rest = url.pathname.replace(/^\/+/, '');
+ } else rest = raw.replace(/^github\.com\//i, '');
+ const parts = rest.replace(/\/+$/, '').split('/').filter(Boolean);
+ const owner = parts[0];
+ const repo = parts[1]?.replace(/\.git$/i, '');
+ if (!owner || !repo || !NAME.test(owner) || !NAME.test(repo))
+ throw new GitHubError('Expected a repository in the form owner/name', 400);
+ let branch: string | null = null;
+ let subdirectory: string | null = null;
+ if ((parts[2] === 'tree' || parts[2] === 'blob') && parts[3]) {
+ // Branch names may contain slashes; the first segment is taken and can be corrected in the form.
+ branch = decodeURIComponent(parts[3]);
+ if (parts.length > 4) subdirectory = parts.slice(4).map(decodeURIComponent).join('/');
+ }
+ return { owner, repo, branch, subdirectory };
+}
+
+export interface GitHubRepoInfo {
+ owner: string;
+ repo: string;
+ fullName: string;
+ description: string | null;
+ defaultBranch: string;
+ branches: string[];
+ private: boolean;
+ sizeKb: number;
+ htmlUrl: string;
+}
+
+const MAX_REPO_KB = 1024 * 1024; // 1 GB as reported by GitHub
+
+export class GitHubClient {
+ private octokit: Octokit;
+
+ constructor(opts: { token?: string | null; fetch?: typeof fetch } = {}) {
+ this.octokit = new Octokit({
+ auth: opts.token || undefined,
+ userAgent: 'vectora-local-engine',
+ request: opts.fetch ? { fetch: opts.fetch } : undefined,
+ retry: { enabled: false },
+ throttle: { enabled: false },
+ log: { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined },
+ });
+ }
+
+ private wrap(err: unknown, hasToken: boolean): GitHubError {
+ const status = (err as { status?: number }).status ?? 502;
+ if (status === 404)
+ return new GitHubError(
+ hasToken
+ ? 'Repository not found, or the credential has no access to it'
+ : 'Repository not found. If it is private, add an access token.',
+ 404,
+ );
+ if (status === 401) return new GitHubError('GitHub rejected the access token', 401);
+ if (status === 403 || status === 429)
+ return new GitHubError(
+ 'GitHub refused the request (rate limit or insufficient permission). An access token raises the limit.',
+ 403,
+ );
+ return new GitHubError(`GitHub could not be reached (${status})`, 502);
+ }
+
+ async describe(ref: Pick, hasToken = false): Promise {
+ try {
+ const { data } = await this.octokit.rest.repos.get({ owner: ref.owner, repo: ref.repo });
+ let branches: string[] = [];
+ try {
+ const res = await this.octokit.rest.repos.listBranches({
+ owner: ref.owner,
+ repo: ref.repo,
+ per_page: 100,
+ });
+ branches = res.data.map((b) => b.name);
+ } catch {
+ branches = [];
+ }
+ if (!branches.includes(data.default_branch)) branches.unshift(data.default_branch);
+ return {
+ owner: data.owner.login,
+ repo: data.name,
+ fullName: data.full_name,
+ description: data.description,
+ defaultBranch: data.default_branch,
+ branches,
+ private: data.private,
+ sizeKb: data.size,
+ htmlUrl: data.html_url,
+ };
+ } catch (err) {
+ throw this.wrap(err, hasToken);
+ }
+ }
+
+ /** Downloads the source archive of a branch to `destFile`. */
+ async downloadArchive(
+ ref: Pick,
+ branch: string,
+ destFile: string,
+ hasToken = false,
+ ): Promise {
+ const info = await this.describe(ref, hasToken);
+ if (info.sizeKb > MAX_REPO_KB)
+ throw new GitHubError('Repository is larger than the 1 GB import limit', 413);
+ try {
+ const res = await this.octokit.request('GET /repos/{owner}/{repo}/zipball/{ref}', {
+ owner: ref.owner,
+ repo: ref.repo,
+ ref: branch,
+ });
+ const buf = Buffer.from(res.data as ArrayBuffer);
+ fs.writeFileSync(destFile, buf);
+ return buf.length;
+ } catch (err) {
+ throw this.wrap(err, hasToken);
+ }
+ }
+}
diff --git a/apps/local-engine/src/main.ts b/apps/local-engine/src/main.ts
new file mode 100644
index 0000000..162c0f7
--- /dev/null
+++ b/apps/local-engine/src/main.ts
@@ -0,0 +1,23 @@
+import { loadConfig } from './config';
+import { buildServer } from './server';
+
+const config = loadConfig();
+const { app } = await buildServer(config);
+
+const shutdown = async (signal: string) => {
+ app.log.info(`${signal} received, shutting down`);
+ await app.close();
+ process.exit(0);
+};
+process.on('SIGINT', () => void shutdown('SIGINT'));
+process.on('SIGTERM', () => void shutdown('SIGTERM'));
+
+try {
+ await app.listen({ host: config.host, port: config.port });
+ app.log.info(
+ `VECTORA engine ready at http://${config.host}:${config.port} (data: ${config.dataDir}${config.webDist ? ', serving web application' : ''})`,
+ );
+} catch (err) {
+ app.log.error(err);
+ process.exit(1);
+}
diff --git a/apps/local-engine/src/provider.ts b/apps/local-engine/src/provider.ts
new file mode 100644
index 0000000..558bda4
--- /dev/null
+++ b/apps/local-engine/src/provider.ts
@@ -0,0 +1,76 @@
+/**
+ * Optional narrative provider.
+ *
+ * Disabled by default. When the user enables it and supplies an endpoint that
+ * speaks the widely used "chat completions" HTTP format, the statements that
+ * were already retrieved from the graph are sent to it to be rewritten as
+ * prose. Only those statements (file paths, symbol names, short evidence
+ * lines) are sent — never whole files — and the provider cannot add facts:
+ * the evidence list shown to the user always comes from the graph.
+ */
+import type { ExplorationAnswer } from '@vectora/shared';
+
+export interface ProviderConfig {
+ endpoint: string;
+ model: string;
+ apiKey: string | null;
+ fetch?: typeof fetch;
+}
+
+export interface NarrativeProvider {
+ summarize(answer: ExplorationAnswer): Promise;
+}
+
+export function createChatCompletionsProvider(cfg: ProviderConfig): NarrativeProvider {
+ return {
+ async summarize(answer) {
+ const url = new URL(cfg.endpoint);
+ if (url.protocol !== 'https:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
+ throw new Error('The provider endpoint must use HTTPS');
+ const facts = answer.statements
+ .slice(0, 60)
+ .map(
+ (s, i) =>
+ `${i + 1}. ${s.verified ? '[verified]' : '[derived]'} ${s.text}${s.evidence[0] ? ` (${s.evidence[0].path}:${s.evidence[0].startLine})` : ''}`,
+ )
+ .join('\n');
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), 30_000);
+ try {
+ const res = await (cfg.fetch ?? fetch)(url, {
+ method: 'POST',
+ signal: controller.signal,
+ headers: {
+ 'content-type': 'application/json',
+ ...(cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {}),
+ },
+ body: JSON.stringify({
+ model: cfg.model,
+ temperature: 0,
+ messages: [
+ {
+ role: 'system',
+ content:
+ 'Rewrite the numbered findings about a software repository as a short, plain explanation. Use only the findings. Keep file references. Say clearly which points are derived rather than verified. Do not add anything that is not in the findings.',
+ },
+ {
+ role: 'user',
+ content: `Question: ${answer.question}\n\nFindings:\n${facts}\n\nKnown limits:\n${answer.caveats.join('\n') || 'none'}`,
+ },
+ ],
+ }),
+ });
+ if (!res.ok) throw new Error(`provider returned ${res.status}`);
+ const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
+ const text = data.choices?.[0]?.message?.content?.trim();
+ if (!text) throw new Error('provider returned no text');
+ return text.slice(0, 6000);
+ } finally {
+ clearTimeout(timer);
+ }
+ },
+ };
+}
+
+export const summarizeWithProvider = (cfg: ProviderConfig, answer: ExplorationAnswer): Promise =>
+ createChatCompletionsProvider(cfg).summarize(answer);
diff --git a/apps/local-engine/src/security.ts b/apps/local-engine/src/security.ts
new file mode 100644
index 0000000..b576ae5
--- /dev/null
+++ b/apps/local-engine/src/security.ts
@@ -0,0 +1,356 @@
+/**
+ * Security helpers: credential encryption at rest, safe archive extraction
+ * and path containment checks.
+ */
+import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+import { pipeline } from 'node:stream/promises';
+import ignore from 'ignore';
+import yauzl from 'yauzl';
+import { isBinaryPath } from '@vectora/language-adapters';
+
+// ---------------------------------------------------------------------------
+// Credentials: AES-256-GCM with a key file kept in the data directory.
+// ---------------------------------------------------------------------------
+
+export class CredentialVault {
+ private key: Buffer;
+
+ constructor(dataDir: string) {
+ const file = path.join(dataDir, 'vault.key');
+ if (fs.existsSync(file)) {
+ this.key = Buffer.from(fs.readFileSync(file, 'utf8').trim(), 'base64');
+ if (this.key.length !== 32) throw new Error('Credential key file is corrupt');
+ } else {
+ fs.mkdirSync(dataDir, { recursive: true });
+ this.key = randomBytes(32);
+ fs.writeFileSync(file, this.key.toString('base64'), { mode: 0o600 });
+ }
+ }
+
+ encrypt(plain: string): string {
+ const iv = randomBytes(12);
+ const cipher = createCipheriv('aes-256-gcm', this.key, iv);
+ const data = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
+ return [
+ 'v1',
+ iv.toString('base64'),
+ cipher.getAuthTag().toString('base64'),
+ data.toString('base64'),
+ ].join(':');
+ }
+
+ decrypt(stored: string): string {
+ const [version, iv, tag, data] = stored.split(':');
+ if (version !== 'v1' || !iv || !tag || !data) throw new Error('Unsupported credential format');
+ const decipher = createDecipheriv('aes-256-gcm', this.key, Buffer.from(iv, 'base64'));
+ decipher.setAuthTag(Buffer.from(tag, 'base64'));
+ return Buffer.concat([decipher.update(Buffer.from(data, 'base64')), decipher.final()]).toString('utf8');
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Path containment
+// ---------------------------------------------------------------------------
+
+/** Resolves `rel` under `root` and guarantees the result does not escape it. */
+export function resolveInside(root: string, rel: string): string | null {
+ if (rel.includes('\0')) return null;
+ const base = path.resolve(root);
+ const target = path.resolve(base, rel);
+ const relative = path.relative(base, target);
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return null;
+ return target;
+}
+
+// ---------------------------------------------------------------------------
+// ZIP handling
+// ---------------------------------------------------------------------------
+
+export interface ZipLimits {
+ maxEntries: number;
+ maxTotalBytes: number;
+ maxFileBytes: number;
+ /** Uncompressed / compressed ratio above which an entry is rejected as a decompression bomb. */
+ maxRatio: number;
+}
+
+export const DEFAULT_ZIP_LIMITS: ZipLimits = {
+ maxEntries: 100_000,
+ maxTotalBytes: 2 * 1024 * 1024 * 1024,
+ maxFileBytes: 8 * 1024 * 1024,
+ maxRatio: 200,
+};
+
+export class ArchiveError extends Error {
+ constructor(
+ message: string,
+ public code: 'invalid-archive' | 'too-many-entries' | 'too-large' | 'empty',
+ ) {
+ super(message);
+ }
+}
+
+export interface ZipEntryInfo {
+ path: string;
+ size: number;
+ directory: boolean;
+}
+
+/** Why an archive entry is not safe to extract, or null when it is. */
+export function unsafeEntryReason(name: string): string | null {
+ if (!name || name.includes('\0')) return 'empty or NUL in name';
+ const n = name.replace(/\\/g, '/');
+ if (n.startsWith('/') || /^[a-zA-Z]:/.test(n)) return 'absolute path';
+ const parts = n.split('/').filter((p) => p !== '');
+ if (parts.some((p) => p === '..')) return 'parent directory traversal';
+ // eslint-disable-next-line no-control-regex
+ if (parts.some((p) => /[<>:"|?*\u0000-\u001f]/.test(p))) return 'characters not allowed in file names';
+ if (parts.some((p) => /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i.test(p)))
+ return 'reserved device name';
+ if (parts.some((p) => p.length > 255) || n.length > 1024) return 'name too long';
+ if (parts.some((p) => /[. ]$/.test(p) && p !== '.')) return 'trailing dot or space';
+ return null;
+}
+
+const isSymlinkEntry = (entry: yauzl.Entry): boolean =>
+ ((entry.externalFileAttributes >>> 16) & 0o170000) === 0o120000;
+
+function openZip(file: string): Promise {
+ return new Promise((resolve, reject) => {
+ // strictFileNames rejects backslashes; decodeStrings + validateEntrySizes are yauzl defaults.
+ yauzl.open(file, { lazyEntries: true, autoClose: true, validateEntrySizes: true }, (err, zip) => {
+ if (err || !zip)
+ reject(
+ new ArchiveError(
+ `Not a readable ZIP archive: ${err?.message ?? 'unknown error'}`,
+ 'invalid-archive',
+ ),
+ );
+ else resolve(zip);
+ });
+ });
+}
+
+export function isZipFile(file: string): boolean {
+ try {
+ const fd = fs.openSync(file, 'r');
+ const buf = Buffer.alloc(4);
+ fs.readSync(fd, buf, 0, 4, 0);
+ fs.closeSync(fd);
+ // Local file header, or end-of-central-directory for an empty archive.
+ return (
+ buf[0] === 0x50 && buf[1] === 0x4b && ((buf[2] === 3 && buf[3] === 4) || (buf[2] === 5 && buf[3] === 6))
+ );
+ } catch {
+ return false;
+ }
+}
+
+export interface ZipInspection {
+ entries: number;
+ files: number;
+ totalBytes: number;
+ rejected: { path: string; reason: string }[];
+ /** Candidate project roots inside the archive ('' is the archive root). */
+ roots: { path: string; files: number; markers: string[] }[];
+ tree: ZipEntryInfo[];
+}
+
+const ROOT_MARKERS = new Set([
+ 'package.json',
+ 'pyproject.toml',
+ 'setup.py',
+ 'requirements.txt',
+ 'go.mod',
+ 'cargo.toml',
+ 'pom.xml',
+ 'build.gradle',
+ 'build.gradle.kts',
+ 'cmakelists.txt',
+ 'makefile',
+ '.gitignore',
+ 'readme.md',
+ 'tsconfig.json',
+]);
+
+/** Lists an archive without extracting it and suggests project roots. */
+export async function inspectZip(
+ file: string,
+ limits: ZipLimits = DEFAULT_ZIP_LIMITS,
+): Promise {
+ if (!isZipFile(file)) throw new ArchiveError('The file is not a ZIP archive', 'invalid-archive');
+ const zip = await openZip(file);
+ const result: ZipInspection = { entries: 0, files: 0, totalBytes: 0, rejected: [], roots: [], tree: [] };
+ const dirFiles = new Map();
+ const dirMarkers = new Map>();
+ const top = new Map();
+ await new Promise((resolve, reject) => {
+ zip.on('error', (e) =>
+ reject(new ArchiveError(`Archive is damaged: ${(e as Error).message}`, 'invalid-archive')),
+ );
+ zip.on('end', resolve);
+ zip.on('entry', (entry: yauzl.Entry) => {
+ result.entries++;
+ if (result.entries > limits.maxEntries) {
+ zip.close();
+ return reject(
+ new ArchiveError(`Archive has more than ${limits.maxEntries} entries`, 'too-many-entries'),
+ );
+ }
+ const name = entry.fileName.replace(/\\/g, '/');
+ const directory = name.endsWith('/');
+ const reason = unsafeEntryReason(name) ?? (isSymlinkEntry(entry) ? 'symbolic link' : null);
+ if (reason) {
+ if (result.rejected.length < 50) result.rejected.push({ path: name.slice(0, 200), reason });
+ } else if (!directory) {
+ result.files++;
+ result.totalBytes += entry.uncompressedSize;
+ if (result.totalBytes > limits.maxTotalBytes) {
+ zip.close();
+ return reject(
+ new ArchiveError('Archive is larger than the allowed uncompressed size', 'too-large'),
+ );
+ }
+ const parts = name.split('/');
+ const base = parts[parts.length - 1]!.toLowerCase();
+ for (let d = 0; d < Math.min(parts.length, 4); d++) {
+ const dir = parts.slice(0, d).join('/');
+ dirFiles.set(dir, (dirFiles.get(dir) ?? 0) + 1);
+ }
+ const dir = parts.slice(0, -1).join('/');
+ if (ROOT_MARKERS.has(base) && parts.length <= 4)
+ (dirMarkers.get(dir) ?? dirMarkers.set(dir, new Set()).get(dir)!).add(parts[parts.length - 1]!);
+ // Two-level preview of the structure.
+ const key = parts.slice(0, 2).join('/');
+ if (top.size < 300 && !top.has(key))
+ top.set(key, {
+ path: key,
+ size: parts.length <= 2 ? entry.uncompressedSize : 0,
+ directory: parts.length > 2,
+ });
+ if (parts.length > 1 && !top.has(parts[0]!))
+ top.set(parts[0]!, { path: parts[0]!, size: 0, directory: true });
+ }
+ zip.readEntry();
+ });
+ zip.readEntry();
+ });
+ if (result.files === 0) throw new ArchiveError('The archive contains no files', 'empty');
+ // A single wrapping directory (as produced by GitHub and most tools) is the natural root.
+ const topDirs = [...dirFiles.keys()].filter((d) => d && !d.includes('/'));
+ const rootFiles = dirFiles.get('') ?? 0;
+ const nested = topDirs.length === 1 && dirFiles.get(topDirs[0]!) === rootFiles ? topDirs[0]! : '';
+ const candidates = new Set([nested, ...dirMarkers.keys()]);
+ result.roots = [...candidates]
+ .map((p) => ({ path: p, files: dirFiles.get(p) ?? 0, markers: [...(dirMarkers.get(p) ?? [])].sort() }))
+ .filter((r) => r.files > 0)
+ .sort((a, b) =>
+ a.path === nested
+ ? -1
+ : b.path === nested
+ ? 1
+ : b.markers.length - a.markers.length || a.path.length - b.path.length,
+ )
+ .slice(0, 12);
+ result.tree = [...top.values()].sort((a, b) => (a.path < b.path ? -1 : 1));
+ return result;
+}
+
+export interface ExtractOptions {
+ /** Only extract entries under this archive path; it is stripped from the output paths. */
+ root?: string;
+ ignorePatterns?: string[];
+ limits?: Partial;
+}
+
+export interface ExtractResult {
+ files: number;
+ bytes: number;
+ skipped: { unsafe: number; symlinks: number; ignored: number; binary: number; oversized: number };
+}
+
+/**
+ * Extracts an archive into `destDir`. Entries that would escape the
+ * destination, symbolic links, ignored paths, binary files and oversized
+ * files are skipped and counted — they are never written to disk.
+ */
+export async function extractZip(
+ file: string,
+ destDir: string,
+ opts: ExtractOptions = {},
+): Promise {
+ if (!isZipFile(file)) throw new ArchiveError('The file is not a ZIP archive', 'invalid-archive');
+ const limits = { ...DEFAULT_ZIP_LIMITS, ...opts.limits };
+ const root = (opts.root ?? '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
+ const ig = ignore().add(['node_modules/', '.git/', ...(opts.ignorePatterns ?? [])]);
+ const dest = path.resolve(destDir);
+ fs.mkdirSync(dest, { recursive: true });
+ const zip = await openZip(file);
+ const result: ExtractResult = {
+ files: 0,
+ bytes: 0,
+ skipped: { unsafe: 0, symlinks: 0, ignored: 0, binary: 0, oversized: 0 },
+ };
+ let entries = 0;
+ await new Promise((resolve, reject) => {
+ const fail = (e: Error) => {
+ zip.close();
+ reject(e);
+ };
+ zip.on('error', (e) =>
+ reject(new ArchiveError(`Archive is damaged: ${(e as Error).message}`, 'invalid-archive')),
+ );
+ zip.on('end', resolve);
+ zip.on('entry', (entry: yauzl.Entry) => {
+ if (++entries > limits.maxEntries)
+ return fail(
+ new ArchiveError(`Archive has more than ${limits.maxEntries} entries`, 'too-many-entries'),
+ );
+ const name = entry.fileName.replace(/\\/g, '/');
+ const next = () => zip.readEntry();
+ if (name.endsWith('/')) return next();
+ if (unsafeEntryReason(name)) return (result.skipped.unsafe++, next());
+ if (isSymlinkEntry(entry)) return (result.skipped.symlinks++, next());
+ if (root && !name.startsWith(`${root}/`)) return next();
+ const rel = root ? name.slice(root.length + 1) : name;
+ if (!rel) return next();
+ if (ig.ignores(rel)) return (result.skipped.ignored++, next());
+ if (isBinaryPath(rel)) return (result.skipped.binary++, next());
+ const tooBig = entry.uncompressedSize > limits.maxFileBytes;
+ const bomb =
+ entry.compressedSize > 0 &&
+ entry.uncompressedSize / entry.compressedSize > limits.maxRatio &&
+ entry.uncompressedSize > 1024 * 1024;
+ if (tooBig || bomb) return (result.skipped.oversized++, next());
+ if (result.bytes + entry.uncompressedSize > limits.maxTotalBytes)
+ return fail(new ArchiveError('Archive is larger than the allowed uncompressed size', 'too-large'));
+ const target = resolveInside(dest, rel);
+ if (!target) return (result.skipped.unsafe++, next());
+ zip.openReadStream(entry, (err, stream) => {
+ if (err || !stream)
+ return fail(
+ new ArchiveError(`Archive entry could not be read: ${err?.message ?? ''}`, 'invalid-archive'),
+ );
+ fs.mkdirSync(path.dirname(target), { recursive: true });
+ // `wx`: never overwrite; an entry listed twice cannot replace an earlier one.
+ pipeline(stream, fs.createWriteStream(target, { flags: 'wx', mode: 0o644 }))
+ .then(() => {
+ result.files++;
+ result.bytes += entry.uncompressedSize;
+ next();
+ })
+ .catch((e: NodeJS.ErrnoException) =>
+ e.code === 'EEXIST'
+ ? next()
+ : fail(new ArchiveError(`Extraction failed: ${e.message}`, 'invalid-archive')),
+ );
+ });
+ });
+ zip.readEntry();
+ });
+ if (result.files === 0)
+ throw new ArchiveError('No source files could be extracted from the archive', 'empty');
+ return result;
+}
diff --git a/apps/local-engine/src/server.ts b/apps/local-engine/src/server.ts
new file mode 100644
index 0000000..a27f695
--- /dev/null
+++ b/apps/local-engine/src/server.ts
@@ -0,0 +1,329 @@
+/**
+ * HTTP API of the local engine. Listens on the loopback interface only,
+ * rejects cross-origin and DNS-rebinding requests, and never logs request
+ * bodies or credentials.
+ */
+import fs from 'node:fs';
+import { pipeline } from 'node:stream/promises';
+import cors from '@fastify/cors';
+import multipart from '@fastify/multipart';
+import fastifyStatic from '@fastify/static';
+import Fastify, { type FastifyInstance, type FastifyReply } from 'fastify';
+import { z } from 'zod';
+import { NODE_TYPES } from '@vectora/graph-schema';
+import { SUPPORTED_LANGUAGES, findPython } from '@vectora/language-adapters';
+import { APP_VERSION, type SessionState, redactSecrets } from '@vectora/shared';
+import type { EngineConfig } from './config';
+import { GitHubError } from './github';
+import { Engine, EngineError } from './service';
+
+const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
+const MAX_UPLOAD_BYTES = 512 * 1024 * 1024;
+
+const hostName = (header: string | undefined): string => {
+ if (!header) return '';
+ const h = header.trim().toLowerCase();
+ return h.startsWith('[') ? h.slice(0, h.indexOf(']') + 1) : h.split(':')[0]!;
+};
+
+export async function buildServer(config: EngineConfig): Promise<{ app: FastifyInstance; engine: Engine }> {
+ const app = Fastify({
+ bodyLimit: 2 * 1024 * 1024,
+ logger:
+ config.logLevel === 'silent'
+ ? false
+ : {
+ level: config.logLevel,
+ // Request bodies are never logged; headers that can carry secrets are removed.
+ redact: {
+ paths: ['req.headers.authorization', 'req.headers.cookie', 'req.headers["x-api-key"]'],
+ remove: true,
+ },
+ serializers: {
+ req: (req) => ({
+ method: req.method,
+ url: redactSecrets(req.url.split('?')[0] ?? ''),
+ id: req.id,
+ }),
+ // Stack traces are kept out of release logs.
+ err: (err: Error) => ({
+ type: err.name,
+ message: redactSecrets(err.message),
+ stack: config.production ? '' : redactSecrets(err.stack ?? ''),
+ }),
+ },
+ },
+ });
+ const engine = new Engine(config, {
+ info: (m) => app.log.info(redactSecrets(m)),
+ warn: (m) => app.log.warn(redactSecrets(m)),
+ error: (m) => app.log.error(redactSecrets(m)),
+ });
+ app.addHook('onClose', async () => engine.close());
+
+ const allowedOrigins = new Set(config.allowedOrigins);
+ await app.register(cors, {
+ origin: (origin, cb) => cb(null, !origin || allowedOrigins.has(origin)),
+ methods: ['GET', 'POST', 'PUT', 'DELETE'],
+ credentials: false,
+ });
+ await app.register(multipart, { limits: { fileSize: MAX_UPLOAD_BYTES, files: 1, fields: 4 } });
+
+ app.addHook('onRequest', async (req, reply) => {
+ // DNS-rebinding protection: the Host header must name the loopback interface.
+ const host = hostName(req.headers.host);
+ if (!LOOPBACK_HOSTS.has(host) && host !== config.host)
+ return reply.code(403).send({ error: 'Forbidden host' });
+ // Cross-site request protection: browsers send Origin on cross-origin and state-changing requests.
+ const origin = req.headers.origin;
+ if (origin) {
+ let originHost: string;
+ try {
+ originHost = new URL(origin).host;
+ } catch {
+ return reply.code(403).send({ error: 'Forbidden origin' });
+ }
+ if (originHost !== req.headers.host && !allowedOrigins.has(origin))
+ return reply.code(403).send({ error: 'Forbidden origin' });
+ }
+ if (
+ req.method !== 'GET' &&
+ req.method !== 'HEAD' &&
+ req.method !== 'OPTIONS' &&
+ req.headers['sec-fetch-site'] === 'cross-site'
+ )
+ return reply.code(403).send({ error: 'Forbidden origin' });
+ });
+ app.addHook('onSend', async (_req, reply) => {
+ reply.header('X-Content-Type-Options', 'nosniff');
+ reply.header('Referrer-Policy', 'no-referrer');
+ reply.header('X-Frame-Options', 'DENY');
+ reply.header('Cross-Origin-Resource-Policy', 'same-origin');
+ reply.header(
+ 'Content-Security-Policy',
+ "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; worker-src 'self' blob:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'",
+ );
+ });
+
+ app.setErrorHandler((err: Error & { statusCode?: number; validation?: unknown }, req, reply) => {
+ if (err instanceof EngineError || err instanceof GitHubError)
+ return reply.code(err.status).send({ error: err.message });
+ if (err instanceof z.ZodError)
+ return reply
+ .code(400)
+ .send({
+ error: 'Invalid request',
+ details: err.issues.map((i) => `${i.path.join('.')}: ${i.message}`),
+ });
+ if (err.statusCode && err.statusCode < 500)
+ return reply.code(err.statusCode).send({ error: err.message });
+ req.log.error({ err }, 'request failed');
+ return reply.code(500).send({ error: 'Internal error' });
+ });
+
+ const idParams = z.object({ id: z.string().uuid() });
+
+ // ---- meta ---------------------------------------------------------------
+ app.get('/api/health', async () => ({
+ status: 'ok',
+ version: APP_VERSION,
+ node: process.version,
+ python: !!findPython(process.env.VECTORA_PYTHON),
+ sampleAvailable: !!config.sampleDir,
+ languages: SUPPORTED_LANGUAGES.map((l) => ({
+ id: l.id,
+ name: l.name,
+ tier: l.tier,
+ extensions: l.extensions,
+ })),
+ }));
+
+ // ---- repositories -------------------------------------------------------
+ app.get('/api/repositories', async () => engine.listRepositories());
+ app.get('/api/repositories/:id', async (req) => engine.getRepository(idParams.parse(req.params).id));
+ app.delete('/api/repositories/:id', async (req, reply) => {
+ engine.deleteRepository(idParams.parse(req.params).id);
+ return reply.code(204).send();
+ });
+ app.post('/api/repositories/:id/reindex', async (req) => {
+ const body = z.object({ full: z.boolean().optional() }).parse(req.body ?? {});
+ return engine.reindex(idParams.parse(req.params).id, body.full ?? false);
+ });
+
+ // ---- imports ------------------------------------------------------------
+ app.post('/api/import/local', async (req) => {
+ const body = z
+ .object({ path: z.string().min(1).max(1024), name: z.string().max(120).optional() })
+ .parse(req.body);
+ return engine.importLocal(body.path, body.name);
+ });
+ app.post('/api/import/sample', async () => engine.importSample());
+ app.get('/api/fs/list', async (req) =>
+ engine.listDirectory(z.object({ path: z.string().max(1024).optional() }).parse(req.query).path),
+ );
+
+ app.post('/api/import/zip/inspect', async (req) => {
+ const part = await req.file();
+ if (!part) throw new EngineError('No file was uploaded', 400);
+ const { id, file } = engine.newUploadPath();
+ await pipeline(part.file, fs.createWriteStream(file, { flags: 'wx' }));
+ if (part.file.truncated) {
+ fs.rmSync(file, { force: true });
+ throw new EngineError('The archive is larger than the 512 MB upload limit', 413);
+ }
+ return engine.registerUpload(id, file, part.filename ?? 'archive.zip');
+ });
+ app.post('/api/import/zip', async (req) => {
+ const body = z
+ .object({
+ uploadId: z.string().uuid(),
+ root: z.string().max(512).optional(),
+ name: z.string().max(120).optional(),
+ })
+ .parse(req.body);
+ return engine.importZip(body.uploadId, body.root, body.name);
+ });
+
+ const githubBody = z.object({
+ url: z.string().min(1).max(500),
+ branch: z.string().max(250).nullish(),
+ subdirectory: z.string().max(500).nullish(),
+ token: z.string().max(400).nullish(),
+ rememberToken: z.boolean().optional(),
+ });
+ app.post('/api/import/github/validate', async (req) => {
+ const body = githubBody.pick({ url: true, token: true }).parse(req.body);
+ return engine.validateGitHub(body.url, body.token);
+ });
+ app.post('/api/import/github', async (req) => engine.importGitHub(githubBody.parse(req.body)));
+
+ // ---- jobs ---------------------------------------------------------------
+ app.get('/api/jobs', async () => engine.activeJobs());
+ app.get('/api/jobs/:id', async (req) => engine.getJob(idParams.parse(req.params).id));
+
+ // ---- graph and analysis -------------------------------------------------
+ app.get('/api/repositories/:id/graph', async (req) => {
+ const q = z.object({ detail: z.enum(['full', 'structure']).optional() }).parse(req.query);
+ return engine.getGraph(idParams.parse(req.params).id, q.detail ?? 'full');
+ });
+ app.get('/api/repositories/:id/members', async (req) => {
+ const q = z.object({ path: z.string().min(1).max(1024) }).parse(req.query);
+ return engine.getFileMembers(idParams.parse(req.params).id, q.path);
+ });
+ app.get('/api/repositories/:id/overview', async (req) => engine.getOverview(idParams.parse(req.params).id));
+ app.get('/api/repositories/:id/analysis', async (req) => engine.getAnalysis(idParams.parse(req.params).id));
+ app.get('/api/repositories/:id/source', async (req) => {
+ const q = z
+ .object({
+ path: z.string().min(1).max(1024),
+ start: z.coerce.number().int().min(1).optional(),
+ end: z.coerce.number().int().min(1).optional(),
+ })
+ .parse(req.query);
+ return engine.getSource(idParams.parse(req.params).id, q.path, q.start, q.end);
+ });
+ app.get('/api/repositories/:id/docs', async (req) => engine.listDocs(idParams.parse(req.params).id));
+ app.get('/api/repositories/:id/search', async (req) => {
+ const q = z
+ .object({
+ q: z.string().max(200),
+ types: z.string().max(400).optional(),
+ limit: z.coerce.number().int().min(1).max(100).optional(),
+ })
+ .parse(req.query);
+ const types = q.types?.split(',').filter((t) => (NODE_TYPES as readonly string[]).includes(t));
+ return engine.search(idParams.parse(req.params).id, q.q, types?.length ? types : undefined, q.limit);
+ });
+ app.post('/api/repositories/:id/ask', async (req) => {
+ const body = z.object({ question: z.string().min(2).max(500) }).parse(req.body);
+ return engine.ask(idParams.parse(req.params).id, body.question);
+ });
+ app.get('/api/repositories/:id/export/:format', async (req, reply) => {
+ const p = z
+ .object({ id: z.string().uuid(), format: z.enum(['json', 'graphml', 'markdown', 'architecture']) })
+ .parse(req.params);
+ const q = z.object({ includeSource: z.enum(['true', 'false']).optional() }).parse(req.query);
+ const out = engine.export(p.id, p.format, q.includeSource === 'true');
+ return reply
+ .header('Content-Type', `${out.contentType}; charset=utf-8`)
+ .header('Content-Disposition', `attachment; filename="${out.fileName}"`)
+ .send(out.body);
+ });
+
+ // ---- sessions -----------------------------------------------------------
+ const sessionBody = z.object({
+ view: z.string().max(40),
+ selectedIds: z.array(z.string().max(1200)).max(500),
+ focusId: z.string().max(1200).nullable(),
+ expandedIds: z.array(z.string().max(1200)).max(5000),
+ hiddenNodeTypes: z.array(z.string().max(40)).max(40),
+ hiddenEdgeTypes: z.array(z.string().max(40)).max(40),
+ layouts: z.record(z.string(), z.unknown()),
+ savedAt: z.string().optional(),
+ });
+ app.get(
+ '/api/repositories/:id/session',
+ async (req) => engine.getSession(idParams.parse(req.params).id) ?? null,
+ );
+ app.put('/api/repositories/:id/session', { bodyLimit: 16 * 1024 * 1024 }, async (req, reply) => {
+ engine.saveSession(idParams.parse(req.params).id, sessionBody.parse(req.body) as unknown as SessionState);
+ return reply.code(204).send();
+ });
+
+ // ---- settings, credentials, local data ---------------------------------
+ app.get('/api/settings', async () => engine.getSettings());
+ app.put('/api/settings', async (req) => engine.updateSettings(req.body));
+ app.get('/api/credentials', async () => engine.listCredentials());
+ const credParams = z.object({ name: z.enum(['github', 'exploration-provider']) });
+ app.put('/api/credentials/:name', async (req, reply) => {
+ engine.setCredential(
+ credParams.parse(req.params).name,
+ z.object({ value: z.string().min(1).max(4096) }).parse(req.body).value,
+ );
+ return reply.code(204).send();
+ });
+ app.delete('/api/credentials/:name', async (req, reply) => {
+ engine.deleteCredential(credParams.parse(req.params).name);
+ return reply.code(204).send();
+ });
+ app.get('/api/data', async () => engine.dataInfo());
+ app.delete('/api/data', async (_req, reply) => {
+ engine.deleteAllData();
+ return reply.code(204).send();
+ });
+ app.delete('/api/cache', async (_req, reply) => {
+ engine.clearCache();
+ return reply.code(204).send();
+ });
+
+ // ---- web application ----------------------------------------------------
+ if (config.webDist) {
+ await app.register(fastifyStatic, {
+ root: config.webDist,
+ wildcard: true,
+ index: false,
+ maxAge: '1h',
+ immutable: false,
+ });
+ // The application shell must never be cached: it points at content-hashed assets that change with every build.
+ const shell = (reply: FastifyReply) =>
+ reply.sendFile('index.html', { cacheControl: false }).header('Cache-Control', 'no-cache');
+ app.get('/', (_req, reply) => shell(reply));
+ app.setNotFoundHandler((req, reply) => {
+ // Client-side routes fall back to the application shell; missing assets and API paths are real 404s.
+ const path = req.url.split('?')[0] ?? '';
+ if (
+ req.method === 'GET' &&
+ !path.startsWith('/api/') &&
+ !path.startsWith('/assets/') &&
+ !/\.[a-z0-9]{2,5}$/i.test(path)
+ )
+ return shell(reply);
+ return reply.code(404).send({ error: 'Not found' });
+ });
+ } else {
+ app.setNotFoundHandler((_req, reply) => reply.code(404).send({ error: 'Not found' }));
+ }
+
+ return { app, engine };
+}
diff --git a/apps/local-engine/src/service.ts b/apps/local-engine/src/service.ts
new file mode 100644
index 0000000..1987774
--- /dev/null
+++ b/apps/local-engine/src/service.ts
@@ -0,0 +1,885 @@
+/**
+ * Engine service: repository imports, indexing jobs, graph queries, exports
+ * and local data management. The HTTP layer in `server.ts` is a thin wrapper.
+ */
+import { randomUUID } from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+import {
+ type GraphIndex,
+ type RepositoryAnalysis,
+ Store,
+ type StoredRepository,
+ buildIndex,
+ buildOverview,
+ explore,
+ exportArchitectureDoc,
+ exportGraphML,
+ exportJson,
+ exportMarkdownAnalysis,
+} from '@vectora/graph-engine';
+import {
+ type GraphEdge,
+ type GraphNode,
+ type KnowledgeGraph,
+ STRUCTURAL_TYPES,
+ normalizePath,
+} from '@vectora/graph-schema';
+import type { FileFacts } from '@vectora/language-adapters';
+import { indexRepository } from '@vectora/repository-indexer';
+import { SearchIndex, searchDocuments } from '@vectora/search';
+import {
+ type ExplorationAnswer,
+ INDEX_STAGES,
+ INDEX_STAGE_LABELS,
+ type IndexStage,
+ type JobProgress,
+ type RepositoryOverview,
+ type RepositoryRecord,
+ type SessionState,
+ type Settings,
+ type SourceResponse,
+ mergeSettings,
+ redactSecrets,
+} from '@vectora/shared';
+import type { EngineConfig } from './config';
+import { GitHubClient, type GitHubRef, type GitHubRepoInfo, parseGitHubUrl } from './github';
+import { summarizeWithProvider } from './provider';
+import {
+ ArchiveError,
+ CredentialVault,
+ type ZipInspection,
+ extractZip,
+ inspectZip,
+ resolveInside,
+} from './security';
+
+export class EngineError extends Error {
+ constructor(
+ message: string,
+ public status = 400,
+ ) {
+ super(message);
+ }
+}
+
+interface LoadedRepo {
+ graph: KnowledgeGraph;
+ index: GraphIndex;
+ search: SearchIndex;
+ analysis: RepositoryAnalysis | null;
+ docs: { path: string; content: string }[] | null;
+}
+
+const MAX_SOURCE_BYTES = 2 * 1024 * 1024;
+const toRecord = (r: StoredRepository): RepositoryRecord => {
+ // The absolute path of managed copies is an implementation detail; local paths are shown as the origin already.
+ const { rootPath: _rootPath, managedSource: _managed, ...rest } = r;
+ return rest;
+};
+
+export class Engine {
+ readonly store: Store;
+ readonly vault: CredentialVault;
+ private jobs = new Map();
+ private queue: Promise = Promise.resolve();
+ private loaded = new Map();
+ private uploads = new Map<
+ string,
+ { file: string; name: string; inspection: ZipInspection; createdAt: number }
+ >();
+ readonly sourcesDir: string;
+ readonly uploadsDir: string;
+
+ constructor(
+ readonly config: EngineConfig,
+ private log: {
+ info: (msg: string) => void;
+ warn: (msg: string) => void;
+ error: (msg: string) => void;
+ } = console,
+ ) {
+ fs.mkdirSync(config.dataDir, { recursive: true });
+ this.sourcesDir = path.join(config.dataDir, 'sources');
+ this.uploadsDir = path.join(config.dataDir, 'uploads');
+ fs.mkdirSync(this.sourcesDir, { recursive: true });
+ fs.mkdirSync(this.uploadsDir, { recursive: true });
+ this.store = new Store(path.join(config.dataDir, 'vectora.db'));
+ this.vault = new CredentialVault(config.dataDir);
+ // Jobs do not survive a restart: anything left mid-index is marked as failed.
+ for (const r of this.store.listRepositories())
+ if (r.status === 'indexing' || r.status === 'pending')
+ this.store.updateRepository(r.id, {
+ status: 'failed',
+ error: 'Indexing was interrupted. Re-index to continue.',
+ });
+ this.cleanUploads(0);
+ }
+
+ close(): void {
+ this.store.close();
+ }
+
+ // ---- repositories -------------------------------------------------------
+
+ listRepositories(): RepositoryRecord[] {
+ return this.store.listRepositories().map(toRecord);
+ }
+
+ private repo(id: string): StoredRepository {
+ const r = this.store.getRepository(id);
+ if (!r) throw new EngineError('Repository not found', 404);
+ return r;
+ }
+
+ getRepository(id: string): RepositoryRecord {
+ return toRecord(this.repo(id));
+ }
+
+ deleteRepository(id: string): void {
+ const r = this.repo(id);
+ this.store.deleteRepository(id);
+ this.loaded.delete(id);
+ if (r.managedSource) this.removeManagedDir(r.rootPath);
+ }
+
+ /** Deletes a directory only when it lies inside VECTORA's own sources folder. */
+ private removeManagedDir(dir: string): void {
+ const rel = path.relative(this.sourcesDir, path.resolve(dir));
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return;
+ const top = path.join(this.sourcesDir, rel.split(path.sep)[0]!);
+ fs.rmSync(top, { recursive: true, force: true });
+ }
+
+ private createRepository(
+ fields: Pick<
+ StoredRepository,
+ 'name' | 'sourceKind' | 'origin' | 'branch' | 'subdirectory' | 'rootPath' | 'managedSource'
+ > & { id?: string },
+ ): StoredRepository {
+ const repo: StoredRepository = {
+ id: fields.id ?? randomUUID(),
+ name: fields.name,
+ sourceKind: fields.sourceKind,
+ origin: fields.origin,
+ branch: fields.branch,
+ subdirectory: fields.subdirectory,
+ rootPath: fields.rootPath,
+ managedSource: fields.managedSource,
+ status: 'pending',
+ createdAt: new Date().toISOString(),
+ lastIndexedAt: null,
+ fileCount: 0,
+ symbolCount: 0,
+ relationshipCount: 0,
+ error: null,
+ };
+ this.store.insertRepository(repo);
+ return repo;
+ }
+
+ // ---- jobs ---------------------------------------------------------------
+
+ getJob(id: string): JobProgress {
+ const j = this.jobs.get(id);
+ if (!j) throw new EngineError('Job not found', 404);
+ return j;
+ }
+
+ activeJobs(): JobProgress[] {
+ return [...this.jobs.values()].filter((j) => j.status === 'running');
+ }
+
+ private newJob(repositoryId: string | null): JobProgress {
+ const job: JobProgress = {
+ id: randomUUID(),
+ repositoryId,
+ status: 'running',
+ stage: 'acquire',
+ stageIndex: 0,
+ message: 'Waiting to start',
+ processed: 0,
+ total: 0,
+ startedAt: new Date().toISOString(),
+ finishedAt: null,
+ error: null,
+ summary: null,
+ };
+ this.jobs.set(job.id, job);
+ // Keep the job table small.
+ if (this.jobs.size > 50)
+ for (const [id, j] of this.jobs)
+ if (j.status !== 'running' && this.jobs.size > 40) this.jobs.delete(id);
+ return job;
+ }
+
+ private setStage(job: JobProgress, stage: IndexStage, message?: string, processed = 0, total = 0): void {
+ job.stage = stage;
+ job.stageIndex = INDEX_STAGES.indexOf(stage);
+ job.message = message ?? INDEX_STAGE_LABELS[stage];
+ job.processed = processed;
+ job.total = total;
+ }
+
+ /** Runs jobs one at a time so indexing never competes with itself for memory. */
+ private enqueue(job: JobProgress, work: () => Promise): void {
+ this.queue = this.queue.then(async () => {
+ try {
+ await work();
+ job.status = 'completed';
+ } catch (err) {
+ const message = redactSecrets(err instanceof Error ? err.message : String(err));
+ job.status = 'failed';
+ job.error = message;
+ if (job.repositoryId && this.store.getRepository(job.repositoryId))
+ this.store.updateRepository(job.repositoryId, { status: 'failed', error: message });
+ this.log.error(`Job ${job.id} failed: ${message}`);
+ } finally {
+ job.finishedAt = new Date().toISOString();
+ }
+ });
+ }
+
+ /** Resolves when all queued jobs have finished (used by tests and shutdown). */
+ idle(): Promise {
+ return this.queue;
+ }
+
+ private async runIndex(repo: StoredRepository, job: JobProgress, forceFull = false): Promise {
+ const settings = this.store.getSettings();
+ if (!fs.existsSync(repo.rootPath) || !fs.statSync(repo.rootPath).isDirectory())
+ throw new EngineError('The repository folder no longer exists on this machine', 410);
+ this.store.updateRepository(repo.id, { status: 'indexing', error: null });
+ const useCache =
+ settings.performance.indexingCache && settings.repository.incrementalIndexing && !forceFull;
+ const previous = useCache
+ ? (this.store.loadFacts(repo.id) as Map)
+ : undefined;
+ const before = useCache ? this.store.getAnalysis(repo.id) : null;
+ const result = await indexRepository({
+ previousFingerprint: (repo.status !== 'failed' && before?.fingerprint) || '',
+ repositoryId: repo.id,
+ repositoryName: repo.name,
+ rootDir: repo.rootPath,
+ ignorePatterns: settings.repository.ignorePatterns,
+ maxFileSizeBytes: settings.repository.maxFileSizeKb * 1024,
+ maxFiles: settings.performance.maxFiles,
+ disabledLanguages: settings.repository.disabledLanguages,
+ concurrency: settings.performance.workerConcurrency,
+ previous: previous && previous.size ? previous : undefined,
+ pythonCommand: process.env.VECTORA_PYTHON,
+ onProgress: (stage, message, processed, total) => this.setStage(job, stage, message, processed, total),
+ });
+ if ('unchanged' in result) {
+ // Nothing changed on disk: the stored graph is still exact, so it is kept as it is.
+ this.setStage(job, 'persist', 'No changes since the last index');
+ this.store.updateRepository(repo.id, {
+ status: 'ready',
+ lastIndexedAt: new Date().toISOString(),
+ error: null,
+ });
+ job.summary = before
+ ? {
+ ...before.summary,
+ analyzedFiles: 0,
+ reusedFiles: before.summary.analyzedFiles + before.summary.reusedFiles,
+ durationMs: result.durationMs,
+ incremental: true,
+ }
+ : null;
+ job.message = 'Workspace ready';
+ this.log.info(`Checked ${repo.name}: no changes (${result.durationMs} ms)`);
+ return;
+ }
+ this.setStage(job, 'persist');
+ this.store.saveGraph(repo.id, result.graph);
+ this.store.setAnalysis(repo.id, result.analysis);
+ if (settings.performance.indexingCache) this.store.saveFacts(repo.id, result.facts);
+ else this.store.clearFacts(repo.id);
+ this.store.updateRepository(repo.id, {
+ status: 'ready',
+ lastIndexedAt: result.analysis.generatedAt,
+ fileCount: result.graph.stats.fileCount,
+ symbolCount: result.graph.stats.symbolCount,
+ relationshipCount: result.graph.stats.edgeCount,
+ error: null,
+ });
+ this.loaded.delete(repo.id);
+ job.summary = result.analysis.summary;
+ job.message = 'Workspace ready';
+ this.log.info(
+ `Indexed ${repo.name}: ${result.graph.stats.nodeCount} nodes, ${result.graph.stats.edgeCount} relationships in ${result.analysis.summary.durationMs} ms`,
+ );
+ }
+
+ reindex(id: string, full = false): JobProgress {
+ const repo = this.repo(id);
+ if (this.activeJobs().some((j) => j.repositoryId === id))
+ throw new EngineError('This repository is already being indexed', 409);
+ const job = this.newJob(id);
+ this.enqueue(job, () => this.runIndex(repo, job, full));
+ return job;
+ }
+
+ // ---- imports ------------------------------------------------------------
+
+ importLocal(dirPath: string, name?: string, kind: 'local' | 'sample' = 'local'): JobProgress {
+ const abs = path.resolve(dirPath);
+ let stat: fs.Stats;
+ try {
+ stat = fs.statSync(abs);
+ } catch {
+ throw new EngineError('That folder does not exist', 404);
+ }
+ if (!stat.isDirectory()) throw new EngineError('The path is not a folder', 400);
+ const inside = path.relative(this.config.dataDir, abs);
+ if (!inside.startsWith('..') && !path.isAbsolute(inside))
+ throw new EngineError("VECTORA's own data folder cannot be imported", 400);
+ const repo = this.createRepository({
+ name: (name?.trim() || path.basename(abs) || 'repository').slice(0, 120),
+ sourceKind: kind,
+ origin: kind === 'sample' ? 'Bundled sample repository' : abs,
+ branch: readGitBranch(abs),
+ subdirectory: null,
+ rootPath: abs,
+ managedSource: false,
+ });
+ const job = this.newJob(repo.id);
+ this.enqueue(job, async () => {
+ this.setStage(job, 'acquire', 'Reading local folder');
+ await this.runIndex(repo, job);
+ });
+ return job;
+ }
+
+ importSample(): JobProgress {
+ if (!this.config.sampleDir)
+ throw new EngineError('The sample repository is not part of this installation', 404);
+ return this.importLocal(this.config.sampleDir, 'Orbit Notes (sample)', 'sample');
+ }
+
+ private cleanUploads(maxAgeMs: number): void {
+ try {
+ for (const f of fs.readdirSync(this.uploadsDir)) {
+ const file = path.join(this.uploadsDir, f);
+ if (maxAgeMs === 0 || Date.now() - fs.statSync(file).mtimeMs > maxAgeMs)
+ fs.rmSync(file, { force: true });
+ }
+ } catch {
+ // nothing to clean
+ }
+ for (const [id, u] of this.uploads)
+ if (maxAgeMs === 0 || Date.now() - u.createdAt > maxAgeMs) this.uploads.delete(id);
+ }
+
+ newUploadPath(): { id: string; file: string } {
+ this.cleanUploads(60 * 60 * 1000);
+ const id = randomUUID();
+ return { id, file: path.join(this.uploadsDir, `${id}.zip`) };
+ }
+
+ async registerUpload(
+ id: string,
+ file: string,
+ originalName: string,
+ ): Promise<{ uploadId: string; fileName: string } & ZipInspection> {
+ try {
+ const inspection = await inspectZip(file);
+ // Only the base name is kept; a client-supplied path is never used.
+ const name =
+ path
+ .basename(originalName.replace(/\\/g, '/'))
+ .replace(/[^\w .()-]/g, '_')
+ .slice(0, 120) || 'archive.zip';
+ this.uploads.set(id, { file, name, inspection, createdAt: Date.now() });
+ return { uploadId: id, fileName: name, ...inspection };
+ } catch (err) {
+ fs.rmSync(file, { force: true });
+ if (err instanceof ArchiveError)
+ throw new EngineError(
+ err.message,
+ err.code === 'too-large' || err.code === 'too-many-entries' ? 413 : 400,
+ );
+ throw err;
+ }
+ }
+
+ importZip(uploadId: string, root: string | undefined, name?: string): JobProgress {
+ const upload = this.uploads.get(uploadId);
+ if (!upload) throw new EngineError('The uploaded archive has expired. Upload it again.', 404);
+ const chosen = normalizePath(root ?? upload.inspection.roots[0]?.path ?? '');
+ if (
+ chosen &&
+ !upload.inspection.roots.some((r) => r.path === chosen) &&
+ !upload.inspection.tree.some(
+ (t) => t.directory && (t.path === chosen || t.path.startsWith(`${chosen}/`)),
+ )
+ )
+ throw new EngineError('That folder does not exist in the archive', 400);
+ const id = randomUUID();
+ const dest = path.join(this.sourcesDir, id);
+ const repo = this.createRepository({
+ id,
+ name: (name?.trim() || (chosen ? chosen.split('/').pop()! : upload.name.replace(/\.zip$/i, ''))).slice(
+ 0,
+ 120,
+ ),
+ sourceKind: 'zip',
+ origin: upload.name,
+ branch: null,
+ subdirectory: chosen || null,
+ rootPath: dest,
+ managedSource: true,
+ });
+ const job = this.newJob(repo.id);
+ this.enqueue(job, async () => {
+ this.setStage(job, 'acquire', 'Extracting archive');
+ try {
+ const settings = this.store.getSettings();
+ const r = await extractZip(upload.file, dest, {
+ root: chosen,
+ ignorePatterns: settings.repository.ignorePatterns,
+ limits: { maxFileBytes: settings.repository.maxFileSizeKb * 1024 },
+ });
+ this.setStage(job, 'acquire', `Extracted ${r.files} files`, r.files, r.files);
+ } finally {
+ fs.rmSync(upload.file, { force: true });
+ this.uploads.delete(uploadId);
+ }
+ await this.runIndex(repo, job);
+ });
+ return job;
+ }
+
+ private githubToken(explicit?: string | null): string | null {
+ if (explicit?.trim()) return explicit.trim();
+ const stored = this.store.getCredential('github');
+ if (!stored) return null;
+ try {
+ return this.vault.decrypt(stored);
+ } catch {
+ return null;
+ }
+ }
+
+ async validateGitHub(
+ url: string,
+ token?: string | null,
+ ): Promise {
+ const ref = parseGitHubUrl(url);
+ const tok = this.githubToken(token);
+ const client = new GitHubClient({ token: tok, fetch: this.config.fetch });
+ const info = await client.describe(ref, !!tok);
+ return { ...info, requested: ref, usingStoredCredential: !token?.trim() && !!tok };
+ }
+
+ importGitHub(input: {
+ url: string;
+ branch?: string | null;
+ subdirectory?: string | null;
+ token?: string | null;
+ rememberToken?: boolean;
+ }): JobProgress {
+ const ref = parseGitHubUrl(input.url);
+ const token = this.githubToken(input.token);
+ if (input.rememberToken && input.token?.trim()) this.setCredential('github', input.token.trim());
+ const sub = normalizePath(input.subdirectory ?? ref.subdirectory ?? '');
+ if (sub.split('/').some((p) => p === '..')) throw new EngineError('Invalid subdirectory', 400);
+ const id = randomUUID();
+ const dest = path.join(this.sourcesDir, id);
+ const repo = this.createRepository({
+ id,
+ name: sub ? `${ref.repo}/${sub}` : ref.repo,
+ sourceKind: 'github',
+ origin: `https://github.com/${ref.owner}/${ref.repo}`,
+ branch: input.branch ?? ref.branch,
+ subdirectory: sub || null,
+ rootPath: sub ? path.join(dest, ...sub.split('/')) : dest,
+ managedSource: true,
+ });
+ const job = this.newJob(repo.id);
+ this.enqueue(job, async () => {
+ const client = new GitHubClient({ token, fetch: this.config.fetch });
+ this.setStage(job, 'acquire', 'Contacting GitHub');
+ const info = await client.describe(ref, !!token);
+ const branch = input.branch ?? ref.branch ?? info.defaultBranch;
+ this.store.updateRepository(repo.id, { branch });
+ const archive = path.join(this.uploadsDir, `${id}.zip`);
+ try {
+ this.setStage(job, 'acquire', `Downloading ${info.fullName}@${branch}`);
+ await client.downloadArchive(ref, branch, archive, !!token);
+ this.setStage(job, 'acquire', 'Extracting archive');
+ const inspection = await inspectZip(archive);
+ const settings = this.store.getSettings();
+ // GitHub archives wrap everything in one `-` directory.
+ await extractZip(archive, dest, {
+ root: inspection.roots[0]?.path ?? '',
+ ignorePatterns: settings.repository.ignorePatterns,
+ limits: { maxFileBytes: settings.repository.maxFileSizeKb * 1024 },
+ });
+ } finally {
+ fs.rmSync(archive, { force: true });
+ }
+ if (sub && !fs.existsSync(repo.rootPath))
+ throw new EngineError(`The folder "${sub}" does not exist on branch ${branch}`, 404);
+ await this.runIndex({ ...repo, branch }, job);
+ });
+ return job;
+ }
+
+ // ---- filesystem browsing for the local import dialog -------------------
+
+ listDirectory(dir?: string): {
+ path: string;
+ parent: string | null;
+ entries: { name: string; path: string; repository: boolean }[];
+ roots: string[];
+ } {
+ const roots: string[] = [];
+ if (process.platform === 'win32') {
+ for (const letter of 'CDEFGHIJKLMNOPQRSTUVWXYZAB')
+ if (fs.existsSync(`${letter}:\\`)) roots.push(`${letter}:\\`);
+ } else roots.push('/');
+ const start = dir?.trim() ? path.resolve(dir) : this.config.installRoot;
+ let entries: { name: string; path: string; repository: boolean }[];
+ try {
+ entries = fs
+ .readdirSync(start, { withFileTypes: true })
+ .filter((d) => d.isDirectory() && !d.name.startsWith('$') && d.name !== 'System Volume Information')
+ .map((d) => {
+ const p = path.join(start, d.name);
+ let repository: boolean;
+ try {
+ repository = ['.git', 'package.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'pom.xml'].some(
+ (m) => fs.existsSync(path.join(p, m)),
+ );
+ } catch {
+ repository = false;
+ }
+ return { name: d.name, path: p, repository };
+ })
+ .sort((a, b) => a.name.localeCompare(b.name));
+ } catch {
+ throw new EngineError('That folder cannot be read', 403);
+ }
+ const parent = path.dirname(start);
+ return { path: start, parent: parent === start ? null : parent, entries, roots };
+ }
+
+ // ---- graph access -------------------------------------------------------
+
+ private load(id: string): LoadedRepo {
+ const cached = this.loaded.get(id);
+ if (cached) return cached;
+ this.repo(id);
+ const graph = this.store.loadGraph(id);
+ const entry: LoadedRepo = {
+ graph,
+ index: buildIndex(graph),
+ search: new SearchIndex(graph.nodes),
+ analysis: this.store.getAnalysis(id),
+ docs: null,
+ };
+ // Memory-aware: keep at most two repositories in memory.
+ while (this.loaded.size >= 2) this.loaded.delete(this.loaded.keys().next().value!);
+ this.loaded.set(id, entry);
+ return entry;
+ }
+
+ getGraph(id: string, detail: 'full' | 'structure' = 'full'): KnowledgeGraph & { partial: boolean } {
+ const { graph } = this.load(id);
+ if (detail === 'full') return { ...graph, partial: false };
+ const keep = new Set(STRUCTURAL_TYPES);
+ const nodes = graph.nodes.filter((n) => keep.has(n.type));
+ const ids = new Set(nodes.map((n) => n.id));
+ return {
+ ...graph,
+ nodes,
+ edges: graph.edges.filter((e) => ids.has(e.source) && ids.has(e.target)),
+ partial: true,
+ };
+ }
+
+ /** Symbols of one file with every relationship touching them (lazy expansion). */
+ getFileMembers(id: string, filePath: string): { nodes: GraphNode[]; edges: GraphEdge[] } {
+ const { graph, index } = this.load(id);
+ const p = normalizePath(filePath);
+ const nodes = graph.nodes.filter(
+ (n) => n.filePath === p && n.type !== 'File' && n.type !== 'Configuration',
+ );
+ const edges = new Map();
+ for (const n of nodes)
+ for (const e of [...(index.out.get(n.id) ?? []), ...(index.in.get(n.id) ?? [])]) edges.set(e.id, e);
+ return { nodes, edges: [...edges.values()] };
+ }
+
+ getOverview(id: string): RepositoryOverview {
+ const l = this.load(id);
+ return buildOverview(this.getRepository(id), l.graph, l.analysis, l.index);
+ }
+
+ getAnalysis(id: string): RepositoryAnalysis | null {
+ return this.load(id).analysis;
+ }
+
+ getSource(id: string, filePath: string, startLine?: number, endLine?: number): SourceResponse {
+ const repo = this.repo(id);
+ const { index } = this.load(id);
+ const p = normalizePath(filePath);
+ // Only files that are part of the index can be read.
+ const node = index.nodes.get(`file:${p}`);
+ if (!node) throw new EngineError('That file is not part of the indexed repository', 404);
+ const abs = resolveInside(repo.rootPath, p);
+ if (!abs) throw new EngineError('Invalid path', 400);
+ let real: string;
+ try {
+ real = fs.realpathSync(abs);
+ } catch {
+ throw new EngineError('The file no longer exists on disk. Re-index the repository.', 410);
+ }
+ const rootReal = fs.realpathSync(repo.rootPath);
+ const rel = path.relative(rootReal, real);
+ if (rel.startsWith('..') || path.isAbsolute(rel)) throw new EngineError('Invalid path', 400);
+ const stat = fs.statSync(real);
+ if (!stat.isFile()) throw new EngineError('Not a file', 400);
+ const truncated = stat.size > MAX_SOURCE_BYTES;
+ const fd = fs.openSync(real, 'r');
+ const buf = Buffer.alloc(Math.min(stat.size, MAX_SOURCE_BYTES));
+ fs.readSync(fd, buf, 0, buf.length, 0);
+ fs.closeSync(fd);
+ const all = buf.toString('utf8').split(/\r?\n/);
+ const s = Math.max(1, Math.min(startLine ?? 1, all.length));
+ const e = Math.max(s, Math.min(endLine ?? all.length, all.length));
+ return {
+ path: p,
+ language: node.language,
+ content: all.slice(s - 1, e).join('\n'),
+ startLine: s,
+ endLine: e,
+ totalLines: all.length,
+ truncated,
+ };
+ }
+
+ private docs(id: string): { path: string; content: string }[] {
+ const l = this.load(id);
+ if (l.docs) return l.docs;
+ const repo = this.repo(id);
+ const docs: { path: string; content: string }[] = [];
+ for (const n of l.graph.nodes) {
+ if (
+ n.type !== 'File' ||
+ !n.filePath ||
+ !['markdown', 'restructuredtext', 'text'].includes(n.language ?? '')
+ )
+ continue;
+ if (docs.length >= 300) break;
+ const abs = resolveInside(repo.rootPath, n.filePath);
+ try {
+ if (abs && fs.statSync(abs).size <= 512 * 1024)
+ docs.push({ path: n.filePath, content: fs.readFileSync(abs, 'utf8') });
+ } catch {
+ // file vanished since indexing
+ }
+ }
+ l.docs = docs;
+ return docs;
+ }
+
+ listDocs(id: string): { path: string; title: string; lines: number }[] {
+ return this.docs(id)
+ .map((d) => ({
+ path: d.path,
+ title: /^#\s+(.+)$/m.exec(d.content)?.[1]?.trim() ?? d.path.split('/').pop()!,
+ lines: d.content.split('\n').length,
+ }))
+ .sort((a, b) => {
+ const rank = (p: string) => (/^readme/i.test(p) ? 0 : p.includes('/') ? 2 : 1);
+ return rank(a.path) - rank(b.path) || (a.path < b.path ? -1 : 1);
+ });
+ }
+
+ search(id: string, query: string, types?: string[], limit = 30) {
+ const l = this.load(id);
+ const hits = l.search.search(query, { types: types as never, limit: Math.min(limit, 100) });
+ return {
+ query,
+ results: hits.map((h) => ({
+ id: h.node.id,
+ name: h.node.name,
+ type: h.node.type,
+ filePath: h.node.filePath,
+ line: h.node.location?.startLine ?? null,
+ description: h.node.description ?? null,
+ score: h.score,
+ match: h.match,
+ })),
+ documents: searchDocuments(this.docs(id), query, 12),
+ };
+ }
+
+ async ask(id: string, question: string): Promise {
+ const l = this.load(id);
+ const answer = explore(question.slice(0, 500), l.index, l.analysis, (q, limit = 10) =>
+ l.search.search(q, { limit }).map((h) => h.node),
+ );
+ const settings = this.store.getSettings();
+ if (
+ settings.exploration.providerEnabled &&
+ settings.exploration.providerEndpoint &&
+ answer.statements.length
+ ) {
+ const stored = this.store.getCredential('exploration-provider');
+ try {
+ answer.narrative = await summarizeWithProvider(
+ {
+ endpoint: settings.exploration.providerEndpoint,
+ model: settings.exploration.providerModel,
+ apiKey: stored ? this.vault.decrypt(stored) : null,
+ fetch: this.config.fetch,
+ },
+ answer,
+ );
+ answer.narrativeSource = new URL(settings.exploration.providerEndpoint).host;
+ } catch (err) {
+ answer.narrative = null;
+ answer.caveats.push(
+ `The optional narrative service did not respond (${redactSecrets((err as Error).message)}). The evidence above is unaffected.`,
+ );
+ }
+ }
+ return answer;
+ }
+
+ export(
+ id: string,
+ format: string,
+ includeSource: boolean,
+ ): { body: string; contentType: string; fileName: string } {
+ const l = this.load(id);
+ const repo = this.getRepository(id);
+ const base = repo.name.replace(/[^\w.-]+/g, '-').slice(0, 60) || 'repository';
+ switch (format) {
+ case 'json':
+ return {
+ body: exportJson(repo, l.graph, { includeSource }),
+ contentType: 'application/json',
+ fileName: `${base}.vectora.json`,
+ };
+ case 'graphml':
+ return {
+ body: exportGraphML(repo, l.graph, { includeSource }),
+ contentType: 'application/xml',
+ fileName: `${base}.graphml`,
+ };
+ case 'markdown':
+ return {
+ body: exportMarkdownAnalysis(this.getOverview(id), l.graph, l.analysis),
+ contentType: 'text/markdown',
+ fileName: `${base}-analysis.md`,
+ };
+ case 'architecture':
+ return {
+ body: exportArchitectureDoc(this.getOverview(id), l.graph, l.analysis),
+ contentType: 'text/markdown',
+ fileName: `${base}-architecture.md`,
+ };
+ default:
+ throw new EngineError('Unknown export format', 400);
+ }
+ }
+
+ // ---- sessions, settings, credentials, data -----------------------------
+
+ getSession(id: string): SessionState | null {
+ this.repo(id);
+ return this.store.getSession(id);
+ }
+
+ saveSession(id: string, session: SessionState): void {
+ this.repo(id);
+ this.store.saveSession(id, { ...session, savedAt: new Date().toISOString() });
+ }
+
+ getSettings(): Settings {
+ return this.store.getSettings();
+ }
+
+ updateSettings(patch: unknown): Settings {
+ const next = mergeSettings(this.store.getSettings(), patch);
+ this.store.saveSettings(next);
+ return next;
+ }
+
+ setCredential(name: string, value: string): void {
+ if (!/^[a-z][a-z0-9-]{1,40}$/.test(name)) throw new EngineError('Invalid credential name', 400);
+ if (!value || value.length > 4096) throw new EngineError('Invalid credential value', 400);
+ this.store.setCredential(name, this.vault.encrypt(value));
+ }
+
+ /** Names and dates only — stored values are never returned. */
+ listCredentials(): { name: string; updatedAt: string }[] {
+ return this.store.listCredentials();
+ }
+
+ deleteCredential(name: string): void {
+ this.store.deleteCredential(name);
+ }
+
+ clearCache(): void {
+ this.store.clearFacts();
+ }
+
+ dataInfo(): {
+ dataDir: string;
+ databaseBytes: number;
+ sourcesBytes: number;
+ repositories: number;
+ credentials: number;
+ } {
+ const size = (p: string): number => {
+ try {
+ const st = fs.statSync(p);
+ if (st.isFile()) return st.size;
+ return fs.readdirSync(p).reduce((a, f) => a + size(path.join(p, f)), 0);
+ } catch {
+ return 0;
+ }
+ };
+ const db = ['vectora.db', 'vectora.db-wal', 'vectora.db-shm'].reduce(
+ (a, f) => a + size(path.join(this.config.dataDir, f)),
+ 0,
+ );
+ return {
+ dataDir: this.config.dataDir,
+ databaseBytes: db,
+ sourcesBytes: size(this.sourcesDir),
+ repositories: this.store.listRepositories().length,
+ credentials: this.store.listCredentials().length,
+ };
+ }
+
+ deleteAllData(): void {
+ if (this.activeJobs().length)
+ throw new EngineError('Wait for indexing to finish before deleting data', 409);
+ this.store.deleteAllData(false);
+ this.loaded.clear();
+ this.jobs.clear();
+ for (const dir of [this.sourcesDir, this.uploadsDir]) {
+ fs.rmSync(dir, { recursive: true, force: true });
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ this.uploads.clear();
+ }
+}
+
+/** Reads the checked-out branch from `.git/HEAD` without running git. */
+export function readGitBranch(dir: string): string | null {
+ try {
+ const head = fs.readFileSync(path.join(dir, '.git', 'HEAD'), 'utf8').trim();
+ const m = /^ref:\s+refs\/heads\/(.+)$/.exec(head);
+ return m ? m[1]! : head.slice(0, 12);
+ } catch {
+ return null;
+ }
+}
diff --git a/apps/web/index.html b/apps/web/index.html
new file mode 100644
index 0000000..f7dfe3b
--- /dev/null
+++ b/apps/web/index.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+ VECTORA — Understand what connects.
+
+
+
+
+
+
+
diff --git a/apps/web/package.json b/apps/web/package.json
new file mode 100644
index 0000000..033147c
--- /dev/null
+++ b/apps/web/package.json
@@ -0,0 +1,62 @@
+{
+ "name": "@vectora/web",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@codemirror/lang-cpp": "^6.0.0",
+ "@codemirror/lang-go": "^6.0.0",
+ "@codemirror/lang-java": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/lang-json": "^6.0.0",
+ "@codemirror/lang-markdown": "^6.0.0",
+ "@codemirror/lang-python": "^6.0.0",
+ "@codemirror/lang-rust": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@dagrejs/dagre": "^3.1.3",
+ "@fontsource/ibm-plex-mono": "^5.0.0",
+ "@fontsource/shippori-mincho": "^5.0.0",
+ "@fontsource/zen-kaku-gothic-new": "^5.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@react-three/drei": "^10.7.9",
+ "@react-three/fiber": "^9.8.2",
+ "@tanstack/react-query": "^5.107.1",
+ "@vectora/graph-layout": "*",
+ "@vectora/graph-schema": "*",
+ "@vectora/search": "*",
+ "@vectora/shared": "*",
+ "@xyflow/react": "^12.12.1",
+ "cmdk": "^1.1.1",
+ "codemirror": "^6.0.2",
+ "graphology": "^0.26.0",
+ "graphology-layout-forceatlas2": "^0.10.1",
+ "graphology-layout-noverlap": "^0.4.2",
+ "html-to-image": "^1.11.13",
+ "lucide-react": "^1.72.0",
+ "radix-ui": "^1.4.0",
+ "react": "^19.3.1",
+ "react-dom": "^19.3.1",
+ "react-markdown": "^10.0.0",
+ "react-router": "^8.5.1",
+ "sigma": "^3.0.3",
+ "three": "^0.187.0",
+ "zustand": "^5.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.5",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@types/three": "^0.187.0",
+ "@vitejs/plugin-react": "^6.3.0",
+ "tailwindcss": "^4.3.5",
+ "vite": "^8.3.8"
+ }
+}
diff --git a/apps/web/public/mark.svg b/apps/web/public/mark.svg
new file mode 100644
index 0000000..5bed6b5
--- /dev/null
+++ b/apps/web/public/mark.svg
@@ -0,0 +1 @@
+
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
new file mode 100644
index 0000000..2defbde
--- /dev/null
+++ b/apps/web/src/App.tsx
@@ -0,0 +1,65 @@
+import { Suspense, lazy } from 'react';
+import { BrowserRouter, Link, Route, Routes } from 'react-router';
+import { SitePage } from './components/Site';
+import { Empty, Loading } from './components/ui';
+import { About } from './pages/About';
+import { ImportPage } from './pages/ImportPage';
+import { Landing } from './pages/Landing';
+import { Dependencies } from './workspace/Dependencies';
+import { Documentation, SourceView } from './workspace/Documentation';
+import { Overview } from './workspace/Overview';
+import { SearchView } from './workspace/SearchView';
+import { SettingsView } from './workspace/SettingsView';
+import { WorkspaceLayout } from './workspace/WorkspaceLayout';
+
+// The three renderers are loaded on demand: each pulls in a sizeable graphics library.
+const GraphExplorer = lazy(() =>
+ import('./workspace/GraphExplorer').then((m) => ({ default: m.GraphExplorer })),
+);
+const Atlas = lazy(() => import('./workspace/Atlas').then((m) => ({ default: m.Atlas })));
+const MindMap = lazy(() => import('./workspace/MindMap').then((m) => ({ default: m.MindMap })));
+
+const view = (el: React.ReactNode) => }>{el};
+
+export function App() {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+
+ }
+ />
+ }>
+ } />
+ )} />
+ )} />
+ )} />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ Return to the start
+
+
+
+ }
+ />
+
+
+ );
+}
diff --git a/apps/web/src/components/CodePreview.tsx b/apps/web/src/components/CodePreview.tsx
new file mode 100644
index 0000000..5fabc36
--- /dev/null
+++ b/apps/web/src/components/CodePreview.tsx
@@ -0,0 +1,13 @@
+import { Suspense, lazy } from 'react';
+import type { CodePreviewProps } from './CodePreviewImpl';
+
+// The editor and its language grammars are loaded when the first source preview is shown.
+const Impl = lazy(() => import('./CodePreviewImpl').then((m) => ({ default: m.CodePreview })));
+
+export function CodePreview(props: CodePreviewProps) {
+ return (
+ Loading source…}>
+
+
+ );
+}
diff --git a/apps/web/src/components/CodePreviewImpl.tsx b/apps/web/src/components/CodePreviewImpl.tsx
new file mode 100644
index 0000000..5af049e
--- /dev/null
+++ b/apps/web/src/components/CodePreviewImpl.tsx
@@ -0,0 +1,131 @@
+import { cpp } from '@codemirror/lang-cpp';
+import { go } from '@codemirror/lang-go';
+import { java } from '@codemirror/lang-java';
+import { javascript } from '@codemirror/lang-javascript';
+import { json } from '@codemirror/lang-json';
+import { markdown } from '@codemirror/lang-markdown';
+import { python } from '@codemirror/lang-python';
+import { rust } from '@codemirror/lang-rust';
+import { HighlightStyle, type LanguageSupport, syntaxHighlighting } from '@codemirror/language';
+import { EditorState, type Extension, RangeSetBuilder } from '@codemirror/state';
+import { Decoration, EditorView, lineNumbers } from '@codemirror/view';
+import { tags as t } from '@lezer/highlight';
+import { useEffect, useRef } from 'react';
+
+// Syntax colours come from the design tokens, so the preview follows the theme.
+const highlight = HighlightStyle.define([
+ {
+ tag: [t.keyword, t.modifier, t.controlKeyword, t.operatorKeyword, t.definitionKeyword, t.moduleKeyword],
+ color: 'var(--indigo)',
+ },
+ { tag: [t.string, t.special(t.string), t.regexp], color: 'var(--clay)' },
+ {
+ tag: [t.comment, t.lineComment, t.blockComment, t.docComment],
+ color: 'var(--text-muted)',
+ fontStyle: 'italic',
+ },
+ { tag: [t.number, t.bool, t.null, t.atom], color: 'var(--warning)' },
+ { tag: [t.typeName, t.className, t.namespace], color: 'var(--text)', fontWeight: '500' },
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.definition(t.variableName)],
+ color: 'var(--text)',
+ },
+ { tag: [t.propertyName, t.variableName], color: 'var(--text-secondary)' },
+ { tag: [t.punctuation, t.bracket, t.operator], color: 'var(--text-muted)' },
+ { tag: t.heading, fontWeight: '500' },
+ { tag: t.invalid, color: 'var(--error)' },
+]);
+
+function languageFor(language: string | null, path: string): LanguageSupport | null {
+ switch (language) {
+ case 'typescript':
+ return javascript({ typescript: true, jsx: /\.tsx$/.test(path) });
+ case 'javascript':
+ return javascript({ jsx: true });
+ case 'python':
+ return python();
+ case 'java':
+ case 'csharp':
+ return java();
+ case 'go':
+ return go();
+ case 'rust':
+ return rust();
+ case 'c':
+ case 'cpp':
+ return cpp();
+ case 'json':
+ return json();
+ case 'markdown':
+ return markdown();
+ default:
+ return null;
+ }
+}
+
+const rangeMark = Decoration.line({ class: 'cm-vectora-range' });
+
+export interface CodePreviewProps {
+ content: string;
+ language: string | null;
+ path: string;
+ /** Line number of the first line of `content` in the original file. */
+ firstLine?: number;
+ /** Lines (original numbering) to mark as the selected range. */
+ highlight?: { start: number; end: number } | null;
+ maxHeight?: number | string;
+ scrollToLine?: number;
+}
+
+/** Read-only source preview. Source text is rendered as text only — never interpreted as markup. */
+export function CodePreview({
+ content,
+ language,
+ path,
+ firstLine = 1,
+ highlight: range,
+ maxHeight = 320,
+ scrollToLine,
+}: CodePreviewProps) {
+ const host = useRef(null);
+ useEffect(() => {
+ if (!host.current) return;
+ const lang = languageFor(language, path);
+ const extensions: Extension[] = [
+ EditorState.readOnly.of(true),
+ EditorView.editable.of(false),
+ EditorView.contentAttributes.of({ 'aria-label': `Source of ${path}`, tabindex: '0' }),
+ lineNumbers({ formatNumber: (n) => String(n + firstLine - 1) }),
+ syntaxHighlighting(highlight),
+ EditorView.theme({
+ '&': { maxHeight: typeof maxHeight === 'number' ? `${maxHeight}px` : maxHeight },
+ '.cm-scroller': { overflow: 'auto' },
+ }),
+ ];
+ if (lang) extensions.push(lang);
+ if (range) {
+ extensions.push(
+ EditorView.decorations.of((view) => {
+ const b = new RangeSetBuilder();
+ const from = Math.max(1, range.start - firstLine + 1);
+ const to = Math.min(view.state.doc.lines, range.end - firstLine + 1);
+ for (let l = from; l <= to; l++)
+ b.add(view.state.doc.line(l).from, view.state.doc.line(l).from, rangeMark);
+ return b.finish();
+ }),
+ );
+ }
+ const view = new EditorView({
+ state: EditorState.create({ doc: content, extensions }),
+ parent: host.current,
+ });
+ if (scrollToLine) {
+ const l = Math.min(view.state.doc.lines, Math.max(1, scrollToLine - firstLine + 1));
+ view.dispatch({
+ effects: EditorView.scrollIntoView(view.state.doc.line(l).from, { y: 'start', yMargin: 48 }),
+ });
+ }
+ return () => view.destroy();
+ }, [content, language, path, firstLine, range?.start, range?.end, maxHeight, scrollToLine]); // eslint-disable-line react-hooks/exhaustive-deps
+ return
;
+}
diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx
new file mode 100644
index 0000000..7a27117
--- /dev/null
+++ b/apps/web/src/components/CommandPalette.tsx
@@ -0,0 +1,178 @@
+import { useQuery } from '@tanstack/react-query';
+import { Command } from 'cmdk';
+import { useEffect, useState } from 'react';
+import { useLocation } from 'react-router';
+import { api } from '@/lib/api';
+import { useWorkspace } from '@/lib/store';
+import { useWs } from '@/workspace/context';
+import { TypeMark } from './ui';
+
+const VIEWS: { view: Parameters['openView']>[0]; label: string }[] = [
+ { view: 'overview', label: 'Overview' },
+ { view: 'mindmap', label: 'Mind Map' },
+ { view: 'graph', label: 'Graph Explorer' },
+ { view: 'atlas', label: '3D Atlas' },
+ { view: 'dependencies', label: 'Dependency Explorer' },
+ { view: 'search', label: 'Search' },
+ { view: 'docs', label: 'Documentation' },
+ { view: 'settings', label: 'Settings' },
+];
+
+const itemClass =
+ 'flex cursor-pointer items-baseline gap-3 border-l-2 border-transparent px-5 py-2 text-[0.875rem] data-[selected=true]:border-ink data-[selected=true]:bg-surface';
+
+/** Global search (Ctrl/Cmd + K). Enter opens the selected result and highlights it in the graph. */
+export function CommandPalette() {
+ const { repo, reveal, openView, settings } = useWs();
+ const open = useWorkspace((s) => s.paletteOpen);
+ const setOpen = useWorkspace((s) => s.setPaletteOpen);
+ const location = useLocation();
+ const [input, setInput] = useState('');
+ const [query, setQuery] = useState('');
+ useEffect(() => {
+ const t = setTimeout(() => setQuery(input.trim()), 140);
+ return () => clearTimeout(t);
+ }, [input]);
+ useEffect(() => {
+ if (!open) setInput('');
+ }, [open]);
+ const { data, isFetching } = useQuery({
+ queryKey: ['search', repo.id, query],
+ queryFn: () => api.search(repo.id, query),
+ enabled: open && query.length >= 2,
+ staleTime: 30_000,
+ placeholderData: (p) => p,
+ });
+
+ const current = location.pathname.split('/')[3] ?? 'overview';
+ const openNode = (id: string) => {
+ setOpen(false);
+ reveal(id);
+ // Stay in the current exploration view; otherwise open the default one.
+ if (!['graph', 'atlas', 'mindmap'].includes(current)) openView(settings.graph.defaultMode);
+ };
+ const results = query.length >= 2 ? (data?.results ?? []) : [];
+ const documents = query.length >= 2 ? (data?.documents ?? []) : [];
+ const views = VIEWS.filter((v) => !query || v.label.toLowerCase().includes(query.toLowerCase()));
+ // Results arrive after typing, so the highlighted item is set explicitly: the first result is
+ // always the one Enter opens.
+ const first =
+ results[0]?.id ??
+ (documents[0]
+ ? `doc:${documents[0].path}:${documents[0].line}`
+ : views[0]
+ ? `view:${views[0].view}`
+ : '');
+ const [selected, setSelected] = useState('');
+ useEffect(() => setSelected(first), [first]);
+
+ return (
+
+
+
+ Esc
+
+
+ {query.length >= 2 && !isFetching ? (
+
+ Nothing in this repository matches “{query}”. Try a file path or part of a symbol name.
+
+ ) : null}
+ {results.length ? (
+
+ {results.map((r) => (
+ openNode(r.id)}
+ className={itemClass}
+ data-testid="palette-result"
+ >
+
+ {r.name}
+ {r.filePath ? (
+
+ {r.filePath}
+ {r.line && r.type !== 'File' ? `:${r.line}` : ''}
+
+ ) : r.description ? (
+ {r.description}
+ ) : null}
+
+
+
+ ))}
+
+ ) : null}
+ {documents.length ? (
+
+ {documents.map((d) => (
+ {
+ setOpen(false);
+ openView('docs', `path=${encodeURIComponent(d.path)}`);
+ }}
+ className={itemClass}
+ >
+
+ {d.snippet}
+
+ {d.path}:{d.line}
+ {d.heading ? ` — ${d.heading}` : ''}
+
+
+
+ ))}
+
+ ) : null}
+ {views.length ? (
+
+ {views.map((v) => (
+ {
+ setOpen(false);
+ openView(v.view);
+ }}
+ className={itemClass}
+ >
+ {v.label}
+
+ ))}
+
+ ) : null}
+
+
+ Enter opens the selected result. Arrow keys move through the list.
+
+
+ );
+}
diff --git a/apps/web/src/components/ExportDialog.tsx b/apps/web/src/components/ExportDialog.tsx
new file mode 100644
index 0000000..3dd5aa3
--- /dev/null
+++ b/apps/web/src/components/ExportDialog.tsx
@@ -0,0 +1,91 @@
+import { useEffect, useState } from 'react';
+import { api, downloadUrl } from '@/lib/api';
+import { useWs } from '@/workspace/context';
+import { Dialog, Notice } from './ui';
+
+const FORMATS: { id: string; label: string; detail: string; graph: boolean }[] = [
+ {
+ id: 'json',
+ label: 'Graph as JSON',
+ detail: 'Every node and relationship with ids, locations, extraction method and confidence.',
+ graph: true,
+ },
+ {
+ id: 'graphml',
+ label: 'Graph as GraphML',
+ detail: 'For Gephi, yEd, Cytoscape and other graph tools.',
+ graph: true,
+ },
+ {
+ id: 'markdown',
+ label: 'Repository analysis (Markdown)',
+ detail: 'Languages, subsystems, entry points, dependencies and unresolved references.',
+ graph: false,
+ },
+ {
+ id: 'architecture',
+ label: 'Architecture documentation (Markdown)',
+ detail: 'Subsystem dependencies, endpoints, models, flows and circular imports.',
+ graph: false,
+ },
+];
+
+export function ExportDialog({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {
+ const { repo, settings } = useWs();
+ const [includeSource, setIncludeSource] = useState(settings.privacy.includeSourceInExports);
+ useEffect(() => {
+ if (open) setIncludeSource(settings.privacy.includeSourceInExports);
+ }, [open, settings.privacy.includeSourceInExports]);
+ return (
+
+
+ {FORMATS.map((f) => (
+
+
+
{f.label}
+
{f.detail}
+
+ downloadUrl('', api.exportUrl(repo.id, f.id, f.graph && includeSource))}
+ data-testid={`export-${f.id}`}
+ >
+ Download
+
+
+ ))}
+
+
+ setIncludeSource(e.target.checked)}
+ />
+
+ Include source text in graph exports
+
+ Adds the evidence lines, signatures and doc comments taken from the repository. Leave this off
+ when sharing a graph of private code.
+
+
+
+ {includeSource ? (
+
+
+ The JSON and GraphML files will contain lines of source code from this repository.
+
+
+ ) : null}
+
+ The Mind Map exports SVG and PNG, and the Graph Explorer and 3D Atlas export PNG, from their own
+ toolbars.
+
+
+ );
+}
diff --git a/apps/web/src/components/Inspector.tsx b/apps/web/src/components/Inspector.tsx
new file mode 100644
index 0000000..29296d0
--- /dev/null
+++ b/apps/web/src/components/Inspector.tsx
@@ -0,0 +1,430 @@
+import { ChevronRight, X } from 'lucide-react';
+import { useCallback, useMemo, useRef, useState } from 'react';
+import {
+ EDGE_TYPE_LABELS,
+ type EdgeType,
+ type GraphEdge,
+ type GraphNode,
+ isSymbolType,
+} from '@vectora/graph-schema';
+import { useSource } from '@/lib/api';
+import { fileIdOf, relationsOf } from '@/lib/graphView';
+import { useWorkspace } from '@/lib/store';
+import { useWs } from '@/workspace/context';
+import { CodePreview } from './CodePreview';
+import { IconButton, TypeMark, cx } from './ui';
+
+const METHOD_LABEL: Record = {
+ filesystem: 'file system structure',
+ manifest: 'package manifest',
+ 'typescript-compiler': 'TypeScript compiler (syntax)',
+ 'typescript-checker': 'TypeScript type checker',
+ 'python-ast': 'Python syntax tree',
+ 'tree-sitter': 'Tree-sitter syntax tree',
+ 'scope-resolution': 'import and scope resolution',
+ 'name-resolution': 'name match within visible files',
+ pattern: 'framework pattern',
+ 'structural-grouping': 'repository layout',
+ 'keyword-evidence': 'identifier and path evidence',
+ aggregation: 'aggregated relationships',
+};
+
+function copy(text: string, done: (label: string) => void, label: string): void {
+ void navigator.clipboard.writeText(text).then(
+ () => done(label),
+ () => done('Copy failed'),
+ );
+}
+
+export function Inspector() {
+ const { adj, repo, reveal, expandFile, openView } = useWs();
+ const primaryId = useWorkspace((s) => s.primaryId);
+ const selectedCount = useWorkspace((s) => s.selectedIds.length);
+ const width = useWorkspace((s) => s.inspectorWidth);
+ const setInspector = useWorkspace((s) => s.setInspector);
+ const setFocus = useWorkspace((s) => s.setFocus);
+ const focusId = useWorkspace((s) => s.focusId);
+ const [toast, setToast] = useState(null);
+ const toastTimer = useRef | null>(null);
+ const notify = useCallback((label: string) => {
+ setToast(label);
+ if (toastTimer.current) clearTimeout(toastTimer.current);
+ toastTimer.current = setTimeout(() => setToast(null), 1600);
+ }, []);
+
+ const node = primaryId ? adj.nodes.get(primaryId) : undefined;
+ const rel = useMemo(() => (node ? relationsOf(node.id, adj) : null), [node, adj]);
+
+ // Source range: the symbol itself, or the head of a file.
+ const isFileLike = node?.type === 'File' || node?.type === 'Configuration';
+ const loc = node?.location ?? null;
+ const start = loc ? (isFileLike ? 1 : Math.max(1, loc.startLine - 2)) : undefined;
+ const end = loc
+ ? isFileLike
+ ? Math.min(loc.endLine, 80)
+ : Math.min(loc.endLine + 2, loc.startLine + 120)
+ : undefined;
+ const hasSource = !!node?.filePath && !!loc && (isFileLike || isSymbolType(node.type));
+ const source = useSource(repo.id, hasSource ? node!.filePath : null, start, end);
+
+ const onResize = (e: React.PointerEvent) => {
+ const startX = e.clientX;
+ const startW = width;
+ const el = e.currentTarget;
+ el.setPointerCapture(e.pointerId);
+ const move = (ev: PointerEvent) =>
+ setInspector(true, Math.max(280, Math.min(720, startW + (startX - ev.clientX))));
+ const up = () => {
+ el.removeEventListener('pointermove', move);
+ el.removeEventListener('pointerup', up);
+ };
+ el.addEventListener('pointermove', move);
+ el.addEventListener('pointerup', up);
+ };
+
+ if (!node || !rel) return null;
+ const dependencyText = () => {
+ const lines = [
+ `${node.name} (${node.type}) — ${node.filePath ?? ''}${loc ? `:${loc.startLine}` : ''}`,
+ '',
+ 'Depends on:',
+ ];
+ for (const e of rel.outgoing)
+ lines.push(
+ ` ${e.type} ${adj.nodes.get(e.target)?.name ?? e.target}${e.location ? ` (${e.location.path}:${e.location.startLine})` : ''}`,
+ );
+ lines.push('', 'Used by:');
+ for (const e of rel.incoming)
+ lines.push(
+ ` ${e.type} ${adj.nodes.get(e.source)?.name ?? e.source}${e.location ? ` (${e.location.path}:${e.location.startLine})` : ''}`,
+ );
+ return lines.join('\n');
+ };
+ const expandRelationships = () => {
+ const files = new Set();
+ const self = fileIdOf(node);
+ if (self) files.add(self);
+ for (const e of [...rel.incoming, ...rel.outgoing]) {
+ const other = adj.nodes.get(e.source === node.id ? e.target : e.source);
+ const fid = other ? fileIdOf(other) : null;
+ if (fid) files.add(fid);
+ }
+ for (const f of files) expandFile(f, true);
+ notify(`${files.size} file${files.size === 1 ? '' : 's'} expanded`);
+ };
+
+ return (
+
+ {
+ if (e.key === 'ArrowLeft') setInspector(true, Math.min(720, width + 24));
+ if (e.key === 'ArrowRight') setInspector(true, Math.max(280, width - 24));
+ }}
+ className="absolute inset-y-0 -left-[3px] z-10 hidden w-[6px] cursor-col-resize hover:bg-indigo/30 focus-visible:bg-indigo/40 md:block"
+ />
+
+
+
+ {node.description ?
{node.description}
: null}
+
+
+ setFocus(focusId === node.id ? null : node.id)}
+ >
+ {focusId === node.id ? 'Exit focus' : 'Focus in graph'}
+
+
+ Expand relationships
+
+ {node.filePath && loc ? (
+
+ openView('source', `path=${encodeURIComponent(node.filePath!)}&line=${loc.startLine}`)
+ }
+ >
+ Open source location
+
+ ) : null}
+ {node.filePath ? (
+ copy(node.filePath!, notify, 'Path copied')}>
+ Copy path
+
+ ) : null}
+ copy(String(node.metadata.qualifiedName ?? node.name), notify, 'Name copied')}
+ >
+ Copy symbol name
+
+ copy(dependencyText(), notify, 'Dependencies copied')}
+ >
+ Copy dependency information
+
+
+
+ {toast}
+
+
+ {hasSource ? (
+
+ {source.isLoading ? (
+ Loading source…
+ ) : source.error ? (
+
+ {(source.error as Error).message}
+
+ ) : source.data ? (
+ <>
+
+ {source.data.endLine < source.data.totalLines &&
+ (isFileLike || (loc && source.data.endLine < loc.endLine)) ? (
+
+ Lines {source.data.startLine}–{source.data.endLine} of {source.data.totalLines}. Open the
+ source location for the full file.
+
+ ) : null}
+ >
+ ) : null}
+
+ ) : null}
+
+
+
+ Found by
+ {METHOD_LABEL[node.extractionMethod] ?? node.extractionMethod}
+ Status
+ {node.verified ? 'Verified from source' : 'Derived interpretation'}
+ Confidence
+ {Math.round(node.confidence * 100)}%
+ {node.language ? (
+ <>
+ Language
+ {node.language}
+ >
+ ) : null}
+ {node.metadata.signature ? (
+ <>
+ Signature
+
+ {String(node.metadata.signature)}
+
+ >
+ ) : null}
+ {node.metadata.entryPoint ? (
+ <>
+ Entry point
+ {String(node.metadata.entryPoint)}
+ >
+ ) : null}
+
+
+
+ {rel.parent ? (
+
+ reveal(rel.parent!.id)} />
+ {rel.subsystem && rel.subsystem.id !== rel.parent.id ? (
+ reveal(rel.subsystem!.id)} />
+ ) : null}
+
+ ) : null}
+
+
+
+
+ {rel.children.length ? (
+
+ {rel.children.slice(0, 40).map((c) => (
+ reveal(c.id)} />
+ ))}
+ {rel.children.length > 40 ? {rel.children.length - 40} more
: null}
+
+ ) : null}
+
+ {node.type === 'Concept' || node.type === 'Subsystem' ? (
+
e.type === 'BELONGS_TO').length}
+ >
+ {(adj.in.get(node.id) ?? [])
+ .filter((e) => e.type === 'BELONGS_TO')
+ .slice(0, 40)
+ .map((e) => {
+ const f = adj.nodes.get(e.source);
+ return f ? (
+
+ reveal(f.id)} derived={!e.verified} />
+
+ ) : null;
+ })}
+
+ ) : null}
+
+ {rel.concepts.length ? (
+
+ {rel.concepts.map((c) => (
+ reveal(c.id)} derived />
+ ))}
+
+ ) : null}
+
+
+ );
+}
+
+function Section({ title, count, children }: { title: string; count?: number; children: React.ReactNode }) {
+ return (
+
+
+ {title}
+ {count !== undefined ? {count} : null}
+
+ {children}
+
+ );
+}
+
+function NodeRow({ node, onClick, derived }: { node: GraphNode; onClick: () => void; derived?: boolean }) {
+ return (
+
+ {node.name}
+ {derived ? derived : null}
+
+
+ );
+}
+
+function Relationships({
+ title,
+ hint,
+ edges,
+ side,
+}: {
+ title: string;
+ hint: string;
+ edges: GraphEdge[];
+ side: 'source' | 'target';
+}) {
+ const { adj, reveal } = useWs();
+ const [open, setOpen] = useState
(null);
+ const groups = useMemo(() => {
+ const m = new Map();
+ for (const e of edges) (m.get(e.type) ?? m.set(e.type, []).get(e.type)!).push(e);
+ return [...m.entries()].sort((a, b) => b[1].length - a[1].length);
+ }, [edges]);
+ return (
+
+ {edges.length === 0 ? None resolved — {hint}.
: null}
+ {groups.map(([type, list]) => (
+
+
+ {EDGE_TYPE_LABELS[type]} ({list.length})
+
+
+ {list.length > 30 ?
{list.length - 30} more not shown
: null}
+
+ ))}
+
+ );
+}
diff --git a/apps/web/src/components/Site.tsx b/apps/web/src/components/Site.tsx
new file mode 100644
index 0000000..6f21903
--- /dev/null
+++ b/apps/web/src/components/Site.tsx
@@ -0,0 +1,229 @@
+import { Moon, Sun } from 'lucide-react';
+import { type ReactNode, useEffect, useRef, useState } from 'react';
+import { Link, NavLink } from 'react-router';
+import { APP_VERSION } from '@vectora/shared';
+import { useSettings } from '@/lib/api';
+import { useReducedMotion, useTheme } from '@/lib/theme';
+import { type Palette, mix, paletteFor } from '@/lib/tokens';
+import { IconButton, Wordmark, cx } from './ui';
+
+/** Theme for pages outside a workspace. Works even when the local engine is not running. */
+export function useSiteTheme(): {
+ theme: 'light' | 'dark';
+ palette: Palette;
+ toggle: () => void;
+ reducedMotion: boolean;
+} {
+ const { settings, update } = useSettings();
+ const [local, setLocal] = useState<'light' | 'dark' | 'system' | undefined>(undefined);
+ const theme = useTheme(settings?.appearance.theme ?? local);
+ const reducedMotion = useReducedMotion(settings);
+ return {
+ theme,
+ palette: paletteFor(theme),
+ reducedMotion,
+ toggle: () => {
+ const next = theme === 'dark' ? 'light' : 'dark';
+ if (settings) update({ appearance: { theme: next } });
+ else setLocal(next);
+ },
+ };
+}
+
+export function SiteHeader() {
+ const { theme, toggle } = useSiteTheme();
+ const link = ({ isActive }: { isActive: boolean }) =>
+ cx(
+ 'text-[0.875rem] no-underline transition-colors duration-200',
+ isActive ? 'text-ink' : 'text-ink-2 hover:text-ink',
+ );
+ return (
+
+
+ Skip to content
+
+
+
+
+
+
+ Repositories
+
+
+ About
+
+
+ Settings
+
+
+ {theme === 'dark' ? : }
+
+
+
+ );
+}
+
+export function SiteFooter() {
+ return (
+
+ );
+}
+
+export function SitePage({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
+
+/**
+ * Illustration of the 3D Atlas: a fixed, generated structure (a centre, six
+ * regions, points around them) drawn in perspective. It is a drawing of the
+ * idea, not data from a repository, and is captioned as such where used.
+ */
+export function AtlasIllustration({ className }: { className?: string }) {
+ const ref = useRef(null);
+ const { palette, reducedMotion } = useSiteTheme();
+ useEffect(() => {
+ const canvas = ref.current;
+ if (!canvas) return;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+ // Deterministic pseudo-random numbers so the drawing is identical on every visit.
+ let seed = 7;
+ const rnd = () => ((seed = (seed * 16807) % 2147483647) - 1) / 2147483646;
+ type P = { x: number; y: number; z: number; r: number; kind: 0 | 1 | 2 };
+ const points: P[] = [{ x: 0, y: 0, z: 0, r: 7, kind: 0 }];
+ const links: [number, number, boolean][] = [];
+ const hubs: number[] = [];
+ for (let h = 0; h < 6; h++) {
+ const a = (h / 6) * Math.PI * 2 + 0.4;
+ const hub: P = { x: Math.cos(a) * 150, y: (rnd() - 0.5) * 110, z: Math.sin(a) * 150, r: 4.5, kind: 1 };
+ hubs.push(points.length);
+ links.push([0, points.length, false]);
+ points.push(hub);
+ const count = 9 + Math.floor(rnd() * 9);
+ for (let i = 0; i < count; i++) {
+ const t = rnd() * Math.PI * 2;
+ const u = Math.acos(2 * rnd() - 1);
+ const d = 26 + rnd() * 42;
+ links.push([hubs[h]!, points.length, false]);
+ points.push({
+ x: hub.x + Math.sin(u) * Math.cos(t) * d,
+ y: hub.y + Math.cos(u) * d,
+ z: hub.z + Math.sin(u) * Math.sin(t) * d,
+ r: 1.4 + rnd() * 1.4,
+ kind: 2,
+ });
+ }
+ }
+ for (let i = 0; i < 14; i++) {
+ const a = 7 + Math.floor(rnd() * (points.length - 7));
+ const b = 7 + Math.floor(rnd() * (points.length - 7));
+ if (a !== b) links.push([a, b, true]);
+ }
+
+ let angle = 0.6;
+ let raf = 0;
+ let visible = true;
+ const draw = () => {
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
+ const w = canvas.clientWidth;
+ const h = canvas.clientHeight;
+ if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
+ canvas.width = w * dpr;
+ canvas.height = h * dpr;
+ }
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ ctx.clearRect(0, 0, w, h);
+ const s = Math.min(w, h) / 470;
+ const cos = Math.cos(angle);
+ const sin = Math.sin(angle);
+ const proj = points.map((p) => {
+ const x = p.x * cos - p.z * sin;
+ const z = p.x * sin + p.z * cos;
+ const y = p.y * 0.94 - z * 0.2;
+ const k = 620 / (620 + z);
+ return { x: w / 2 + x * k * s, y: h / 2 + y * k * s, k, z };
+ });
+ for (const [a, b, cross] of links) {
+ const pa = proj[a]!;
+ const pb = proj[b]!;
+ const depth = Math.max(0, Math.min(1, ((pa.z + pb.z) / 2 + 220) / 440));
+ ctx.strokeStyle = mix(cross ? palette.clay : palette.textMuted, palette.canvas, 0.35 + depth * 0.5);
+ ctx.lineWidth = cross ? 0.9 : 0.7;
+ ctx.setLineDash(cross ? [3, 4] : []);
+ ctx.beginPath();
+ ctx.moveTo(pa.x, pa.y);
+ ctx.lineTo(pb.x, pb.y);
+ ctx.stroke();
+ }
+ ctx.setLineDash([]);
+ points
+ .map((p, i) => ({ p, q: proj[i]! }))
+ .sort((a, b) => b.q.z - a.q.z)
+ .forEach(({ p, q }) => {
+ const depth = Math.max(0, Math.min(1, (q.z + 220) / 440));
+ const base = p.kind === 0 ? palette.text : p.kind === 1 ? palette.indigo : palette.textSecondary;
+ ctx.fillStyle = mix(base, palette.canvas, depth * 0.6);
+ const r = p.r * q.k * s * 1.25;
+ ctx.beginPath();
+ if (p.kind === 1) ctx.rect(q.x - r, q.y - r, r * 2, r * 2);
+ else ctx.arc(q.x, q.y, r, 0, Math.PI * 2);
+ ctx.fill();
+ });
+ };
+ const loop = () => {
+ angle += 0.0011;
+ draw();
+ if (visible && !reducedMotion) raf = requestAnimationFrame(loop);
+ };
+ draw();
+ // Animate only while the drawing is on screen and motion is allowed.
+ const io = new IntersectionObserver(([e]) => {
+ visible = !!e?.isIntersecting;
+ cancelAnimationFrame(raf);
+ if (visible && !reducedMotion) raf = requestAnimationFrame(loop);
+ });
+ io.observe(canvas);
+ const ro = new ResizeObserver(draw);
+ ro.observe(canvas);
+ return () => {
+ cancelAnimationFrame(raf);
+ io.disconnect();
+ ro.disconnect();
+ };
+ }, [palette, reducedMotion]);
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/ViewControls.tsx b/apps/web/src/components/ViewControls.tsx
new file mode 100644
index 0000000..65b84e2
--- /dev/null
+++ b/apps/web/src/components/ViewControls.tsx
@@ -0,0 +1,168 @@
+import { SlidersHorizontal, X } from 'lucide-react';
+import { Popover } from 'radix-ui';
+import { type ReactNode, useMemo } from 'react';
+import {
+ EDGE_TYPES,
+ EDGE_TYPE_LABELS,
+ NODE_TYPES,
+ NODE_TYPE_LABELS,
+ type NodeType,
+} from '@vectora/graph-schema';
+import { useWorkspace } from '@/lib/store';
+import { FAMILY_GLYPH, NODE_FAMILY, nodeColor } from '@/lib/tokens';
+import { useWs } from '@/workspace/context';
+import { cx } from './ui';
+
+/** Toolbar shown above every exploration view: filters, legend, focus and highlight state. */
+export function ViewToolbar({ children, status }: { children?: ReactNode; status?: ReactNode }) {
+ const { adj, view } = useWs();
+ const focusId = useWorkspace((s) => s.focusId);
+ const setFocus = useWorkspace((s) => s.setFocus);
+ const highlight = useWorkspace((s) => s.highlight);
+ const setHighlight = useWorkspace((s) => s.setHighlight);
+ return (
+
+
+
+ {children}
+ {focusId ? (
+ setFocus(null)} data-testid="exit-focus">
+
+ Exit focus on {adj.nodes.get(focusId)?.name ?? 'node'}
+
+ ) : null}
+ {highlight ? (
+ setHighlight(null)}>
+
+ Clear highlight: {highlight.label}
+
+ ) : null}
+
+
+ {view.nodes.length.toLocaleString()} nodes, {view.edges.length.toLocaleString()} relationships
+ {view.collapsedSymbols
+ ? `, ${view.collapsedSymbols.toLocaleString()} symbols folded into their files`
+ : ''}
+ {view.truncated ? ' (limited by the node limit in Settings)' : ''}
+ {status ? <> — {status}> : null}
+
+
+ );
+}
+
+function FilterPopover() {
+ const { graph, palette } = useWs();
+ const hiddenNodeTypes = useWorkspace((s) => s.hiddenNodeTypes);
+ const hiddenEdgeTypes = useWorkspace((s) => s.hiddenEdgeTypes);
+ const setHiddenNodeTypes = useWorkspace((s) => s.setHiddenNodeTypes);
+ const setHiddenEdgeTypes = useWorkspace((s) => s.setHiddenEdgeTypes);
+ const counts = useMemo(() => ({ nodes: graph.stats.nodesByType, edges: graph.stats.edgesByType }), [graph]);
+ const toggle = (list: string[], v: string, set: (l: string[]) => void) =>
+ set(list.includes(v) ? list.filter((x) => x !== v) : [...list, v]);
+ const nodeTypes = NODE_TYPES.filter((t) => (counts.nodes[t] ?? 0) > 0 && t !== 'Repository');
+ const edgeTypes = EDGE_TYPES.filter((t) => (counts.edges[t] ?? 0) > 0);
+ const active = hiddenNodeTypes.length + hiddenEdgeTypes.length;
+ return (
+
+
+
+ Filters and legend
+ {active ? ({active} hidden) : null}
+
+
+
+
+
+ Node types
+ {nodeTypes.map((t) => (
+ toggle(hiddenNodeTypes, t, setHiddenNodeTypes)}
+ label={NODE_TYPE_LABELS[t]}
+ count={counts.nodes[t] ?? 0}
+ swatch={
+
+ {FAMILY_GLYPH[NODE_FAMILY[t]]}
+
+ }
+ />
+ ))}
+
+
+ Relationships
+ {edgeTypes.map((t) => (
+ toggle(hiddenEdgeTypes, t, setHiddenEdgeTypes)}
+ label={EDGE_TYPE_LABELS[t]}
+ count={counts.edges[t] ?? 0}
+ />
+ ))}
+
+
+
+
+
+
+
+ Solid line: relationship verified from source
+
+
+
+
+
+ Dashed or thin line: derived, not verified
+
+
Each node type has its own glyph and size; names are shown as you zoom in.
+
+
+
+
+ );
+}
+
+function FilterRow({
+ checked,
+ onChange,
+ label,
+ count,
+ swatch,
+}: {
+ checked: boolean;
+ onChange: () => void;
+ label: string;
+ count: number;
+ swatch?: ReactNode;
+}) {
+ return (
+
+
+ {swatch}
+ {label}
+ {count.toLocaleString()}
+
+ );
+}
diff --git a/apps/web/src/components/ui.tsx b/apps/web/src/components/ui.tsx
new file mode 100644
index 0000000..43b22be
--- /dev/null
+++ b/apps/web/src/components/ui.tsx
@@ -0,0 +1,279 @@
+import { type ComponentProps, type ReactNode, useId } from 'react';
+import { Dialog as RDialog, Switch as RSwitch } from 'radix-ui';
+import { X } from 'lucide-react';
+import type { GraphNode } from '@vectora/graph-schema';
+import { NODE_TYPE_LABELS } from '@vectora/graph-schema';
+import { FAMILY_GLYPH, NODE_FAMILY } from '@/lib/tokens';
+
+export const cx = (...parts: (string | false | null | undefined)[]): string =>
+ parts.filter(Boolean).join(' ');
+
+/** The brand wordmark. */
+export function Wordmark({ className }: { className?: string }) {
+ return (
+
+ VECTORA
+
+ );
+}
+
+export function Dialog({
+ open,
+ onOpenChange,
+ title,
+ description,
+ children,
+ wide,
+}: {
+ open: boolean;
+ onOpenChange: (o: boolean) => void;
+ title: string;
+ description?: string;
+ children: ReactNode;
+ wide?: boolean;
+}) {
+ return (
+
+
+
+
+
+
+ {title}
+ {description ? (
+ {description}
+ ) : (
+ {title}
+ )}
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+export function Toggle({
+ checked,
+ onChange,
+ label,
+ hint,
+}: {
+ checked: boolean;
+ onChange: (v: boolean) => void;
+ label: string;
+ hint?: string;
+}) {
+ const id = useId();
+ return (
+
+
+ {label}
+ {hint ? {hint} : null}
+
+
+
+
+
+ );
+}
+
+export function SelectField({
+ label,
+ hint,
+ value,
+ onChange,
+ options,
+}: {
+ label: string;
+ hint?: string;
+ value: T;
+ onChange: (v: T) => void;
+ options: { value: T; label: string }[];
+}) {
+ const id = useId();
+ return (
+
+
+ {label}
+ {hint ? {hint} : null}
+
+ onChange(e.target.value as T)}
+ >
+ {options.map((o) => (
+
+ {o.label}
+
+ ))}
+
+
+ );
+}
+
+export function NumberField({
+ label,
+ hint,
+ value,
+ onChange,
+ min,
+ max,
+ step = 1,
+ unit,
+}: {
+ label: string;
+ hint?: string;
+ value: number;
+ onChange: (v: number) => void;
+ min: number;
+ max: number;
+ step?: number;
+ unit?: string;
+}) {
+ const id = useId();
+ return (
+
+
+ {label}
+ {hint ? {hint} : null}
+
+
+ {
+ const v = Number(e.target.value);
+ if (Number.isFinite(v) && v >= min && v <= max) onChange(v);
+ }}
+ />
+ {unit ? {unit} : null}
+
+
+ );
+}
+
+/** Type marker: a glyph plus the type name, so the type never depends on colour alone. */
+export function TypeMark({ node, showLabel = true }: { node: Pick; showLabel?: boolean }) {
+ return (
+
+ {FAMILY_GLYPH[NODE_FAMILY[node.type]]}
+ {showLabel ? (
+ NODE_TYPE_LABELS[node.type]
+ ) : (
+ {NODE_TYPE_LABELS[node.type]}
+ )}
+
+ );
+}
+
+export function Notice({
+ tone = 'info',
+ children,
+ action,
+}: {
+ tone?: 'info' | 'error' | 'warning' | 'success';
+ children: ReactNode;
+ action?: ReactNode;
+}) {
+ const color =
+ tone === 'error'
+ ? 'var(--error)'
+ : tone === 'warning'
+ ? 'var(--warning)'
+ : tone === 'success'
+ ? 'var(--success)'
+ : 'var(--indigo)';
+ const word =
+ tone === 'error' ? 'Error' : tone === 'warning' ? 'Note' : tone === 'success' ? 'Done' : 'Info';
+ return (
+
+
+
+ {word}.
+ {' '}
+ {children}
+
+ {action}
+
+ );
+}
+
+export function Empty({ title, children }: { title: string; children?: ReactNode }) {
+ return (
+
+ );
+}
+
+export function Loading({ label = 'Loading' }: { label?: string }) {
+ return (
+
+ {label}…
+
+ );
+}
+
+export function SectionHeading({
+ children,
+ aside,
+ id,
+}: {
+ children: ReactNode;
+ aside?: ReactNode;
+ id?: string;
+}) {
+ return (
+
+
+ {children}
+
+ {aside ? {aside} : null}
+
+ );
+}
+
+export function IconButton({
+ label,
+ children,
+ className,
+ ...rest
+}: { label: string; children: ReactNode } & ComponentProps<'button'>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts
new file mode 100644
index 0000000..27aa7dc
--- /dev/null
+++ b/apps/web/src/lib/api.ts
@@ -0,0 +1,315 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import type { GraphEdge, GraphNode, KnowledgeGraph } from '@vectora/graph-schema';
+import type {
+ ExplorationAnswer,
+ JobProgress,
+ RepositoryOverview,
+ RepositoryRecord,
+ SessionState,
+ Settings,
+ SourceResponse,
+} from '@vectora/shared';
+
+export class ApiError extends Error {
+ constructor(
+ message: string,
+ public status: number,
+ ) {
+ super(message);
+ }
+}
+
+async function request(url: string, init?: RequestInit): Promise {
+ let res: Response;
+ try {
+ res = await fetch(url, init);
+ } catch {
+ throw new ApiError('The local engine is not reachable. Start it with “npm start” (or “npm run dev”).', 0);
+ }
+ if (res.status === 204) return undefined as T;
+ const isJson = res.headers.get('content-type')?.includes('application/json');
+ if (!res.ok) {
+ const body = isJson ? ((await res.json()) as { error?: string }) : null;
+ throw new ApiError(
+ body?.error ??
+ (res.status >= 500
+ ? 'The local engine is not reachable or failed to answer.'
+ : `Request failed (${res.status})`),
+ res.status,
+ );
+ }
+ return (isJson ? res.json() : res.text()) as Promise;
+}
+
+const json = (method: string, body?: unknown): RequestInit => ({
+ method,
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(body ?? {}),
+});
+
+export interface Health {
+ status: string;
+ version: string;
+ node: string;
+ python: boolean;
+ sampleAvailable: boolean;
+ languages: { id: string; name: string; tier: number; extensions: string[] }[];
+}
+export interface GitHubInfo {
+ fullName: string;
+ description: string | null;
+ defaultBranch: string;
+ branches: string[];
+ private: boolean;
+ sizeKb: number;
+ requested: { branch: string | null; subdirectory: string | null };
+ usingStoredCredential: boolean;
+}
+export interface ZipInspection {
+ uploadId: string;
+ fileName: string;
+ entries: number;
+ files: number;
+ totalBytes: number;
+ rejected: { path: string; reason: string }[];
+ roots: { path: string; files: number; markers: string[] }[];
+ tree: { path: string; size: number; directory: boolean }[];
+}
+export interface DirListing {
+ path: string;
+ parent: string | null;
+ entries: { name: string; path: string; repository: boolean }[];
+ roots: string[];
+}
+export interface SearchResponse {
+ query: string;
+ results: {
+ id: string;
+ name: string;
+ type: GraphNode['type'];
+ filePath: string | null;
+ line: number | null;
+ description: string | null;
+ score: number;
+ match: string;
+ }[];
+ documents: { path: string; line: number; snippet: string; heading: string | null }[];
+}
+export interface AnalysisResponse {
+ generatedAt: string;
+ flows: { id: string; name: string; kind: string; steps: string[]; reachesData: boolean }[];
+ entryPoints: { path: string; nodeId: string; reason: string }[];
+ declaredDependencies: {
+ name: string;
+ version: string | null;
+ manifest: string;
+ ecosystem: string;
+ dev: boolean;
+ line: number;
+ }[];
+ unresolvedCount: number;
+ unresolvedByReason: Record;
+ unresolvedSamples: {
+ fromPath: string;
+ fromSymbol: string | null;
+ kind: string;
+ name: string;
+ line: number;
+ reason: string;
+ }[];
+ parseIssues: { path: string; message: string; line?: number; severity: string }[];
+ skipped: Record;
+ truncated: boolean;
+ validation: { errors: number; warnings: number };
+ communities: number;
+}
+export type GraphResponse = KnowledgeGraph & { partial: boolean };
+
+export const api = {
+ health: () => request('/api/health'),
+ repositories: () => request('/api/repositories'),
+ repository: (id: string) => request(`/api/repositories/${id}`),
+ deleteRepository: (id: string) => request(`/api/repositories/${id}`, { method: 'DELETE' }),
+ reindex: (id: string, full = false) =>
+ request(`/api/repositories/${id}/reindex`, json('POST', { full })),
+ job: (id: string) => request(`/api/jobs/${id}`),
+ importLocal: (path: string, name?: string) =>
+ request('/api/import/local', json('POST', { path, name })),
+ importSample: () => request('/api/import/sample', json('POST')),
+ listDir: (path?: string) =>
+ request(`/api/fs/list${path ? `?path=${encodeURIComponent(path)}` : ''}`),
+ inspectZip: (file: File) => {
+ const form = new FormData();
+ form.append('file', file);
+ return request('/api/import/zip/inspect', { method: 'POST', body: form });
+ },
+ importZip: (uploadId: string, root?: string, name?: string) =>
+ request('/api/import/zip', json('POST', { uploadId, root, name })),
+ // The token travels in the request body over the loopback interface only; it is never put in a URL.
+ validateGitHub: (url: string, token?: string) =>
+ request('/api/import/github/validate', json('POST', { url, token: token || undefined })),
+ importGitHub: (body: {
+ url: string;
+ branch?: string;
+ subdirectory?: string;
+ token?: string;
+ rememberToken?: boolean;
+ }) => request('/api/import/github', json('POST', body)),
+ graph: (id: string, detail: 'full' | 'structure') =>
+ request(`/api/repositories/${id}/graph?detail=${detail}`),
+ members: (id: string, path: string) =>
+ request<{ nodes: GraphNode[]; edges: GraphEdge[] }>(
+ `/api/repositories/${id}/members?path=${encodeURIComponent(path)}`,
+ ),
+ overview: (id: string) => request(`/api/repositories/${id}/overview`),
+ analysis: (id: string) => request(`/api/repositories/${id}/analysis`),
+ source: (id: string, path: string, start?: number, end?: number) =>
+ request(
+ `/api/repositories/${id}/source?path=${encodeURIComponent(path)}${start ? `&start=${start}` : ''}${end ? `&end=${end}` : ''}`,
+ ),
+ docs: (id: string) =>
+ request<{ path: string; title: string; lines: number }[]>(`/api/repositories/${id}/docs`),
+ search: (id: string, q: string, types?: string[]) =>
+ request(
+ `/api/repositories/${id}/search?q=${encodeURIComponent(q)}${types?.length ? `&types=${types.join(',')}` : ''}`,
+ ),
+ ask: (id: string, question: string) =>
+ request(`/api/repositories/${id}/ask`, json('POST', { question })),
+ session: (id: string) => request(`/api/repositories/${id}/session`),
+ saveSession: (id: string, s: SessionState) =>
+ request(`/api/repositories/${id}/session`, json('PUT', s)),
+ settings: () => request('/api/settings'),
+ updateSettings: (patch: unknown) => request('/api/settings', json('PUT', patch)),
+ credentials: () => request<{ name: string; updatedAt: string }[]>('/api/credentials'),
+ setCredential: (name: string, value: string) =>
+ request(`/api/credentials/${name}`, json('PUT', { value })),
+ deleteCredential: (name: string) => request(`/api/credentials/${name}`, { method: 'DELETE' }),
+ dataInfo: () =>
+ request<{
+ dataDir: string;
+ databaseBytes: number;
+ sourcesBytes: number;
+ repositories: number;
+ credentials: number;
+ }>('/api/data'),
+ deleteAllData: () => request('/api/data', { method: 'DELETE' }),
+ clearCache: () => request('/api/cache', { method: 'DELETE' }),
+ exportUrl: (id: string, format: string, includeSource: boolean) =>
+ `/api/repositories/${id}/export/${format}${includeSource ? '?includeSource=true' : ''}`,
+};
+
+// ---- hooks ---------------------------------------------------------------
+
+export const useHealth = () =>
+ useQuery({ queryKey: ['health'], queryFn: api.health, staleTime: 60_000, retry: 1 });
+export const useRepositories = () => useQuery({ queryKey: ['repositories'], queryFn: api.repositories });
+export const useRepository = (id: string) =>
+ useQuery({
+ queryKey: ['repository', id],
+ queryFn: () => api.repository(id),
+ refetchInterval: (q) =>
+ q.state.data?.status === 'indexing' || q.state.data?.status === 'pending' ? 1000 : false,
+ });
+export const useOverview = (id: string, enabled = true) =>
+ useQuery({ queryKey: ['overview', id], queryFn: () => api.overview(id), enabled });
+export const useAnalysis = (id: string) =>
+ useQuery({ queryKey: ['analysis', id], queryFn: () => api.analysis(id) });
+export const useSource = (id: string, path: string | null, start?: number, end?: number) =>
+ useQuery({
+ queryKey: ['source', id, path, start, end],
+ queryFn: () => api.source(id, path!, start, end),
+ enabled: !!path,
+ staleTime: 5 * 60_000,
+ });
+export const useJob = (jobId: string | null) =>
+ useQuery({
+ queryKey: ['job', jobId],
+ queryFn: () => api.job(jobId!),
+ enabled: !!jobId,
+ refetchInterval: (q) => (q.state.data && q.state.data.status !== 'running' ? false : 400),
+ });
+
+export function useSettings() {
+ const qc = useQueryClient();
+ const query = useQuery({ queryKey: ['settings'], queryFn: api.settings, staleTime: Infinity });
+ const mutation = useMutation({
+ mutationFn: api.updateSettings,
+ onMutate: async (patch: Record>) => {
+ // Optimistic: settings take effect immediately and are then confirmed by the engine.
+ const prev = qc.getQueryData(['settings']);
+ if (prev) {
+ const next = structuredClone(prev) as unknown as Record>;
+ for (const [section, values] of Object.entries(patch))
+ next[section] = { ...next[section], ...values };
+ qc.setQueryData(['settings'], next);
+ }
+ return { prev };
+ },
+ onError: (_e, _p, ctx) => ctx?.prev && qc.setQueryData(['settings'], ctx.prev),
+ onSuccess: (data) => qc.setQueryData(['settings'], data),
+ });
+ return {
+ settings: query.data ?? null,
+ update: mutation.mutate,
+ error: mutation.error ?? query.error,
+ saving: mutation.isPending,
+ };
+}
+
+/**
+ * Loads the knowledge graph. Large repositories are loaded progressively:
+ * the structural skeleton first, symbols of a file when it is expanded.
+ */
+export function useGraph(id: string, symbolCount: number, maxNodes: number, enabled = true) {
+ const qc = useQueryClient();
+ const detail: 'full' | 'structure' = symbolCount > maxNodes ? 'structure' : 'full';
+ const query = useQuery({
+ queryKey: ['graph', id, detail],
+ queryFn: () => api.graph(id, detail),
+ enabled,
+ staleTime: Infinity,
+ gcTime: 10 * 60_000,
+ });
+ const loadMembers = async (path: string): Promise => {
+ const current = qc.getQueryData(['graph', id, detail]);
+ if (!current?.partial) return;
+ const extra = await qc.fetchQuery({
+ queryKey: ['members', id, path],
+ queryFn: () => api.members(id, path),
+ staleTime: Infinity,
+ });
+ qc.setQueryData(['graph', id, detail], (g) => {
+ if (!g) return g;
+ const ids = new Set(g.nodes.map((n) => n.id));
+ const nodes = [...g.nodes, ...extra.nodes.filter((n) => !ids.has(n.id))];
+ const all = new Set(nodes.map((n) => n.id));
+ const eids = new Set(g.edges.map((e) => e.id));
+ const edges = [
+ ...g.edges,
+ ...extra.edges.filter((e) => !eids.has(e.id) && all.has(e.source) && all.has(e.target)),
+ ];
+ return { ...g, nodes, edges };
+ });
+ };
+ return {
+ graph: query.data ?? null,
+ isLoading: query.isLoading,
+ error: query.error,
+ partial: query.data?.partial ?? false,
+ loadMembers,
+ };
+}
+
+export function downloadText(fileName: string, text: string, type = 'text/plain'): void {
+ const url = URL.createObjectURL(new Blob([text], { type }));
+ downloadUrl(fileName, url);
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
+}
+export function downloadUrl(fileName: string, url: string): void {
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+}
diff --git a/apps/web/src/lib/graphView.ts b/apps/web/src/lib/graphView.ts
new file mode 100644
index 0000000..2649247
--- /dev/null
+++ b/apps/web/src/lib/graphView.ts
@@ -0,0 +1,195 @@
+/**
+ * Derives what the visualisations show from the full knowledge graph and the
+ * current filters. All three views call this with the same inputs, so they
+ * always present the same underlying graph.
+ */
+import {
+ type EdgeType,
+ type GraphEdge,
+ type GraphNode,
+ type KnowledgeGraph,
+ type NodeType,
+ isSymbolType,
+} from '@vectora/graph-schema';
+
+export interface ViewOptions {
+ density: 'structure' | 'balanced' | 'full';
+ expandedIds: readonly string[];
+ hiddenNodeTypes: readonly string[];
+ hiddenEdgeTypes: readonly string[];
+ showUnverified: boolean;
+ focusId: string | null;
+ focusDepth?: number;
+ maxNodes: number;
+ /** Nodes that must stay visible (selection, highlights). */
+ pinnedIds?: readonly string[];
+}
+
+export interface GraphView {
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+ /** Symbols left out by the density setting (they appear when their file is expanded). */
+ collapsedSymbols: number;
+ truncated: boolean;
+}
+
+export interface Adjacency {
+ nodes: Map;
+ out: Map;
+ in: Map;
+}
+
+export function buildAdjacency(graph: Pick): Adjacency {
+ const nodes = new Map();
+ const out = new Map();
+ const inn = new Map();
+ for (const n of graph.nodes) nodes.set(n.id, n);
+ for (const e of graph.edges) {
+ (out.get(e.source) ?? out.set(e.source, []).get(e.source)!).push(e);
+ (inn.get(e.target) ?? inn.set(e.target, []).get(e.target)!).push(e);
+ }
+ return { nodes, out, in: inn };
+}
+
+const PROMINENT: ReadonlySet = new Set([
+ 'Class',
+ 'DatabaseModel',
+ 'ApiEndpoint',
+ 'Component',
+ 'Interface',
+]);
+const BALANCED_LIMIT = 1200;
+
+export const fileIdOf = (n: GraphNode): string | null => (n.filePath ? `file:${n.filePath}` : null);
+
+export function deriveView(
+ graph: Pick,
+ adj: Adjacency,
+ o: ViewOptions,
+): GraphView {
+ const hiddenTypes = new Set(o.hiddenNodeTypes);
+ const hiddenEdges = new Set(o.hiddenEdgeTypes);
+ const expanded = new Set(o.expandedIds);
+ const pinned = new Set(o.pinnedIds ?? []);
+ const symbolTotal = graph.nodes.reduce((a, n) => a + (isSymbolType(n.type) ? 1 : 0), 0);
+
+ let focusSet: Set | null = null;
+ if (o.focusId && adj.nodes.has(o.focusId)) {
+ focusSet = new Set([o.focusId]);
+ let frontier = [o.focusId];
+ for (let d = 0; d < (o.focusDepth ?? 1); d++) {
+ const next: string[] = [];
+ for (const id of frontier)
+ for (const e of [...(adj.out.get(id) ?? []), ...(adj.in.get(id) ?? [])]) {
+ if (hiddenEdges.has(e.type)) continue;
+ const other = e.source === id ? e.target : e.source;
+ if (!focusSet.has(other)) {
+ focusSet.add(other);
+ next.push(other);
+ }
+ }
+ frontier = next;
+ }
+ // Keep the containing file of every focused symbol for context.
+ for (const id of [...focusSet]) {
+ const f = adj.nodes.get(id);
+ const fid = f ? fileIdOf(f) : null;
+ if (fid && adj.nodes.has(fid)) focusSet.add(fid);
+ }
+ }
+
+ let collapsedSymbols = 0;
+ const visible: GraphNode[] = [];
+ for (const n of graph.nodes) {
+ if (focusSet) {
+ if (focusSet.has(n.id)) visible.push(n);
+ continue;
+ }
+ if (pinned.has(n.id)) {
+ visible.push(n);
+ continue;
+ }
+ if (hiddenTypes.has(n.type)) continue;
+ if (isSymbolType(n.type)) {
+ const fid = fileIdOf(n);
+ const fileExpanded = !!fid && expanded.has(fid);
+ const show =
+ fileExpanded ||
+ o.density === 'full' ||
+ (o.density === 'balanced' && (symbolTotal <= BALANCED_LIMIT || PROMINENT.has(n.type)));
+ if (!show) {
+ collapsedSymbols++;
+ continue;
+ }
+ }
+ visible.push(n);
+ }
+
+ let truncated = false;
+ let nodes = visible;
+ if (nodes.length > o.maxNodes) {
+ truncated = true;
+ // Keep structure first, then the best-connected symbols.
+ const degree = (id: string) => (adj.out.get(id)?.length ?? 0) + (adj.in.get(id)?.length ?? 0);
+ const rank = (n: GraphNode) => (pinned.has(n.id) ? 0 : isSymbolType(n.type) ? 2 : 1);
+ nodes = [...nodes].sort((a, b) => rank(a) - rank(b) || degree(b.id) - degree(a.id)).slice(0, o.maxNodes);
+ }
+ const ids = new Set(nodes.map((n) => n.id));
+ const edges: GraphEdge[] = [];
+ for (const e of graph.edges) {
+ if (hiddenEdges.has(e.type)) continue;
+ if (!o.showUnverified && !e.verified) continue;
+ if (ids.has(e.source) && ids.has(e.target)) edges.push(e);
+ }
+ return { nodes, edges, collapsedSymbols, truncated };
+}
+
+/** Node ids within one hop of `id` over the given edges (the node itself included). */
+export function neighbourhood(
+ id: string,
+ edges: readonly GraphEdge[],
+): { nodes: Set; edges: Set } {
+ const nodes = new Set([id]);
+ const edgeIds = new Set();
+ for (const e of edges) {
+ if (e.source === id || e.target === id) {
+ nodes.add(e.source);
+ nodes.add(e.target);
+ edgeIds.add(e.id);
+ }
+ }
+ return { nodes, edges: edgeIds };
+}
+
+export interface Relations {
+ incoming: GraphEdge[];
+ outgoing: GraphEdge[];
+ parent: GraphNode | null;
+ children: GraphNode[];
+ concepts: GraphNode[];
+ subsystem: GraphNode | null;
+}
+
+const HIERARCHY: ReadonlySet = new Set(['CONTAINS', 'DEFINED_IN', 'BELONGS_TO']);
+
+export function relationsOf(id: string, adj: Adjacency): Relations {
+ const node = adj.nodes.get(id);
+ const out = adj.out.get(id) ?? [];
+ const inn = adj.in.get(id) ?? [];
+ const parentEdge = inn.find((e) => e.type === 'CONTAINS');
+ const fileNode = node && isSymbolType(node.type) ? adj.nodes.get(fileIdOf(node) ?? '') : node;
+ const conceptIds = (fileNode?.metadata.concepts as string[] | undefined) ?? [];
+ const subsystemId =
+ (node?.metadata.subsystem as string | undefined) ?? (fileNode?.metadata.subsystem as string | undefined);
+ return {
+ incoming: inn.filter((e) => !HIERARCHY.has(e.type)),
+ outgoing: out.filter((e) => !HIERARCHY.has(e.type)),
+ parent: parentEdge ? (adj.nodes.get(parentEdge.source) ?? null) : null,
+ children: out
+ .filter((e) => e.type === 'CONTAINS')
+ .map((e) => adj.nodes.get(e.target))
+ .filter((n): n is GraphNode => !!n),
+ concepts: conceptIds.map((c) => adj.nodes.get(c)).filter((n): n is GraphNode => !!n),
+ subsystem: subsystemId ? (adj.nodes.get(subsystemId) ?? null) : null,
+ };
+}
diff --git a/apps/web/src/lib/store.ts b/apps/web/src/lib/store.ts
new file mode 100644
index 0000000..50abe11
--- /dev/null
+++ b/apps/web/src/lib/store.ts
@@ -0,0 +1,132 @@
+/**
+ * Workspace state shared by all three exploration views: selection, focus,
+ * expansion and filters live here so that selecting a node in one view shows
+ * it selected in the others.
+ */
+import { create } from 'zustand';
+import type { SessionState } from '@vectora/shared';
+
+export interface Highlight {
+ label: string;
+ nodeIds: string[];
+ edgeIds: string[];
+}
+
+interface WorkspaceState {
+ repoId: string | null;
+ selectedIds: string[];
+ /** Most recently selected node — the one shown in the inspector. */
+ primaryId: string | null;
+ hoverId: string | null;
+ /** Focus mode: only this node and its neighbourhood are shown. */
+ focusId: string | null;
+ /** Files whose symbols are shown. */
+ expandedIds: string[];
+ hiddenNodeTypes: string[];
+ hiddenEdgeTypes: string[];
+ highlight: Highlight | null;
+ inspectorOpen: boolean;
+ inspectorWidth: number;
+ paletteOpen: boolean;
+ layouts: SessionState['layouts'];
+ /** Bumped whenever a view should re-centre on the primary selection. */
+ revealTick: number;
+ sessionLoaded: boolean;
+
+ select: (id: string, opts?: { additive?: boolean; reveal?: boolean }) => void;
+ clearSelection: () => void;
+ setHover: (id: string | null) => void;
+ setFocus: (id: string | null) => void;
+ toggleExpanded: (id: string, force?: boolean) => void;
+ setHiddenNodeTypes: (t: string[]) => void;
+ setHiddenEdgeTypes: (t: string[]) => void;
+ setHighlight: (h: Highlight | null) => void;
+ setInspector: (open: boolean, width?: number) => void;
+ setPaletteOpen: (open: boolean) => void;
+ setLayout: (key: K, value: SessionState['layouts'][K]) => void;
+ hydrate: (repoId: string, session: SessionState | null, defaults: { hiddenEdgeTypes: string[] }) => void;
+ snapshot: (view: string) => SessionState;
+}
+
+export const DEFAULT_HIDDEN_NODE_TYPES = ['Directory', 'Module', 'Package', 'Configuration'];
+
+export const useWorkspace = create((set, get) => ({
+ repoId: null,
+ selectedIds: [],
+ primaryId: null,
+ hoverId: null,
+ focusId: null,
+ expandedIds: [],
+ hiddenNodeTypes: DEFAULT_HIDDEN_NODE_TYPES,
+ hiddenEdgeTypes: ['DEFINED_IN', 'EXPORTS'],
+ highlight: null,
+ inspectorOpen: true,
+ inspectorWidth: 380,
+ paletteOpen: false,
+ layouts: {},
+ revealTick: 0,
+ sessionLoaded: false,
+
+ select: (id, opts) =>
+ set((s) => {
+ if (opts?.additive) {
+ const has = s.selectedIds.includes(id);
+ const selectedIds = has ? s.selectedIds.filter((x) => x !== id) : [...s.selectedIds, id];
+ return {
+ selectedIds,
+ primaryId: has ? (selectedIds[selectedIds.length - 1] ?? null) : id,
+ inspectorOpen: true,
+ };
+ }
+ return {
+ selectedIds: [id],
+ primaryId: id,
+ inspectorOpen: true,
+ revealTick: opts?.reveal ? s.revealTick + 1 : s.revealTick,
+ };
+ }),
+ clearSelection: () => set({ selectedIds: [], primaryId: null }),
+ setHover: (hoverId) => set((s) => (s.hoverId === hoverId ? s : { hoverId })),
+ setFocus: (focusId) => set((s) => ({ focusId, revealTick: s.revealTick + 1 })),
+ toggleExpanded: (id, force) =>
+ set((s) => {
+ const has = s.expandedIds.includes(id);
+ const want = force ?? !has;
+ if (want === has) return s;
+ return { expandedIds: want ? [...s.expandedIds, id] : s.expandedIds.filter((x) => x !== id) };
+ }),
+ setHiddenNodeTypes: (hiddenNodeTypes) => set({ hiddenNodeTypes }),
+ setHiddenEdgeTypes: (hiddenEdgeTypes) => set({ hiddenEdgeTypes }),
+ setHighlight: (highlight) => set({ highlight }),
+ setInspector: (inspectorOpen, width) =>
+ set((s) => ({ inspectorOpen, inspectorWidth: width ?? s.inspectorWidth })),
+ setPaletteOpen: (paletteOpen) => set({ paletteOpen }),
+ setLayout: (key, value) => set((s) => ({ layouts: { ...s.layouts, [key]: value } })),
+ hydrate: (repoId, session, defaults) =>
+ set({
+ repoId,
+ selectedIds: session?.selectedIds ?? [],
+ primaryId: session?.selectedIds?.[session.selectedIds.length - 1] ?? null,
+ focusId: session?.focusId ?? null,
+ expandedIds: session?.expandedIds ?? [],
+ hiddenNodeTypes: session?.hiddenNodeTypes ?? DEFAULT_HIDDEN_NODE_TYPES,
+ hiddenEdgeTypes: session?.hiddenEdgeTypes ?? defaults.hiddenEdgeTypes,
+ layouts: session?.layouts ?? {},
+ highlight: null,
+ hoverId: null,
+ sessionLoaded: true,
+ }),
+ snapshot: (view) => {
+ const s = get();
+ return {
+ view,
+ selectedIds: s.selectedIds.slice(0, 200),
+ focusId: s.focusId,
+ expandedIds: s.expandedIds.slice(0, 2000),
+ hiddenNodeTypes: s.hiddenNodeTypes,
+ hiddenEdgeTypes: s.hiddenEdgeTypes,
+ layouts: s.layouts,
+ savedAt: new Date().toISOString(),
+ };
+ },
+}));
diff --git a/apps/web/src/lib/theme.ts b/apps/web/src/lib/theme.ts
new file mode 100644
index 0000000..15e0b50
--- /dev/null
+++ b/apps/web/src/lib/theme.ts
@@ -0,0 +1,48 @@
+import { useEffect, useSyncExternalStore } from 'react';
+import type { Settings } from '@vectora/shared';
+import { type Palette, paletteFor } from './tokens';
+
+const media = (q: string) => ({
+ subscribe: (cb: () => void) => {
+ const m = window.matchMedia(q);
+ m.addEventListener('change', cb);
+ return () => m.removeEventListener('change', cb);
+ },
+ get: () => window.matchMedia(q).matches,
+});
+const darkMedia = media('(prefers-color-scheme: dark)');
+const motionMedia = media('(prefers-reduced-motion: reduce)');
+
+export type ThemeSetting = 'light' | 'dark' | 'system';
+
+/** Resolves the theme setting against the system preference and applies it to the document. */
+export function useTheme(setting: ThemeSetting | undefined): 'light' | 'dark' {
+ const systemDark = useSyncExternalStore(darkMedia.subscribe, darkMedia.get, () => false);
+ const stored = (
+ typeof localStorage !== 'undefined' ? localStorage.getItem('vectora-theme') : null
+ ) as ThemeSetting | null;
+ const effective = setting ?? stored ?? 'system';
+ const theme = effective === 'system' ? (systemDark ? 'dark' : 'light') : effective;
+ useEffect(() => {
+ document.documentElement.dataset.theme = theme;
+ if (setting) localStorage.setItem('vectora-theme', setting);
+ }, [theme, setting]);
+ return theme;
+}
+
+/** True when motion should be reduced, from settings or the system preference. */
+export function useReducedMotion(settings: Settings | null): boolean {
+ const system = useSyncExternalStore(motionMedia.subscribe, motionMedia.get, () => false);
+ const pref = settings?.performance.reducedMotion ?? 'system';
+ const reduced =
+ pref === 'on' || settings?.graph.animationIntensity === 'none' || (pref === 'system' && system);
+ useEffect(() => {
+ const el = document.documentElement;
+ if (pref === 'on' || settings?.graph.animationIntensity === 'none') el.dataset.motion = 'reduced';
+ else if (pref === 'off') el.dataset.motion = 'full';
+ else delete el.dataset.motion;
+ }, [pref, settings?.graph.animationIntensity]);
+ return reduced;
+}
+
+export const usePalette = (theme: 'light' | 'dark'): Palette => paletteFor(theme);
diff --git a/apps/web/src/lib/tokens.ts b/apps/web/src/lib/tokens.ts
new file mode 100644
index 0000000..b3a4907
--- /dev/null
+++ b/apps/web/src/lib/tokens.ts
@@ -0,0 +1,181 @@
+/**
+ * Design tokens — the single source of truth for colour. `styles.css` mirrors
+ * these values as CSS custom properties (a unit test keeps the two in step);
+ * WebGL renderers read them from here because they cannot use CSS variables.
+ */
+import type { EdgeType, NodeType } from '@vectora/graph-schema';
+
+export interface Palette {
+ canvas: string;
+ surface: string;
+ elevated: string;
+ text: string;
+ textSecondary: string;
+ textMuted: string;
+ line: string;
+ indigo: string;
+ clay: string;
+ ash: string;
+ success: string;
+ warning: string;
+ error: string;
+ selection: string;
+ relationActive: string;
+ relationUnverified: string;
+}
+
+export const LIGHT: Palette = {
+ canvas: '#F8F7F2',
+ surface: '#F3F1EA', // Washi
+ elevated: '#FBFAF6',
+ text: '#20211F', // Sumi
+ textSecondary: '#5F5D55',
+ textMuted: '#8C897F', // Stone
+ line: '#DCD8CE',
+ indigo: '#34445C',
+ clay: '#A18C72',
+ ash: '#B8B3A7',
+ success: '#55704F',
+ warning: '#7D5C1B',
+ error: '#964436',
+ selection: '#34445C',
+ relationActive: '#34445C',
+ relationUnverified: '#A18C72',
+};
+
+export const DARK: Palette = {
+ canvas: '#171816',
+ surface: '#20211F',
+ elevated: '#272824',
+ text: '#ECEAE2',
+ textSecondary: '#A7A499',
+ textMuted: '#77766D',
+ line: '#373832',
+ indigo: '#8292AD',
+ clay: '#B39A7A',
+ ash: '#5C5B54',
+ success: '#8FAE8A',
+ warning: '#C9A566',
+ error: '#D08C7C',
+ selection: '#8292AD',
+ relationActive: '#8292AD',
+ relationUnverified: '#B39A7A',
+};
+
+export const paletteFor = (theme: 'light' | 'dark'): Palette => (theme === 'dark' ? DARK : LIGHT);
+
+/** Visual family of a node type. Colour is always paired with shape (3D), size and a text label. */
+export type NodeFamily =
+ 'structure' | 'file' | 'type' | 'callable' | 'data' | 'endpoint' | 'concept' | 'external' | 'value';
+
+export const NODE_FAMILY: Record = {
+ Repository: 'structure',
+ Subsystem: 'structure',
+ Module: 'structure',
+ Directory: 'structure',
+ File: 'file',
+ Configuration: 'file',
+ Package: 'external',
+ Class: 'type',
+ Interface: 'type',
+ Type: 'type',
+ Component: 'type',
+ Function: 'callable',
+ Method: 'callable',
+ Variable: 'value',
+ Constant: 'value',
+ ApiEndpoint: 'endpoint',
+ DatabaseModel: 'data',
+ Concept: 'concept',
+};
+
+export const FAMILY_LABEL: Record = {
+ structure: 'Structure',
+ file: 'Files',
+ type: 'Types and classes',
+ callable: 'Functions and methods',
+ data: 'Database models',
+ endpoint: 'API endpoints',
+ concept: 'Concepts',
+ external: 'External packages',
+ value: 'Variables and constants',
+};
+
+/** Single-character glyph shown next to labels so type never depends on colour alone. */
+export const FAMILY_GLYPH: Record = {
+ structure: '◼',
+ file: '▫',
+ type: '◆',
+ callable: '●',
+ data: '▤',
+ endpoint: '▶',
+ concept: '◎',
+ external: '○',
+ value: '·',
+};
+
+export function familyColor(family: NodeFamily, p: Palette): string {
+ switch (family) {
+ case 'structure':
+ return p.text;
+ case 'file':
+ return p.textMuted;
+ case 'type':
+ return p.indigo;
+ case 'callable':
+ return p.textSecondary;
+ case 'data':
+ return p.clay;
+ case 'endpoint':
+ return p.warning;
+ case 'concept':
+ return p.success;
+ case 'external':
+ return p.ash;
+ case 'value':
+ return p.ash;
+ }
+}
+
+export const nodeColor = (type: NodeType, p: Palette): string => familyColor(NODE_FAMILY[type], p);
+
+/** Relative node size by type: repository largest, functions smallest. */
+export const NODE_SIZE: Record = {
+ Repository: 10,
+ Subsystem: 7,
+ Concept: 6,
+ Module: 4.5,
+ Directory: 3.5,
+ Package: 3.5,
+ File: 3.2,
+ Configuration: 2.8,
+ Class: 2.8,
+ DatabaseModel: 2.8,
+ Component: 2.6,
+ Interface: 2.4,
+ ApiEndpoint: 2.4,
+ Type: 2,
+ Function: 1.8,
+ Method: 1.6,
+ Constant: 1.3,
+ Variable: 1.3,
+};
+
+/** Relationship types whose direction expresses a dependency (drawn with arrows). */
+export const STRUCTURE_EDGES: readonly EdgeType[] = ['CONTAINS', 'DEFINED_IN', 'BELONGS_TO', 'EXPORTS'];
+
+export const hexToRgb = (hex: string): [number, number, number] => {
+ const n = parseInt(hex.slice(1), 16);
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
+};
+export const withAlpha = (hex: string, a: number): string => {
+ const [r, g, b] = hexToRgb(hex);
+ return `rgba(${r},${g},${b},${a})`;
+};
+/** Blends `hex` towards `bg` — used to dim unrelated nodes without transparency sorting issues. */
+export const mix = (hex: string, bg: string, t: number): string => {
+ const a = hexToRgb(hex);
+ const b = hexToRgb(bg);
+ const c = a.map((v, i) => Math.round(v + (b[i]! - v) * t));
+ return `#${c.map((v) => v.toString(16).padStart(2, '0')).join('')}`;
+};
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
new file mode 100644
index 0000000..ddfd3f0
--- /dev/null
+++ b/apps/web/src/main.tsx
@@ -0,0 +1,22 @@
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App';
+import './styles.css';
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry: (count, err) => count < 1 && (err as { status?: number }).status !== 404,
+ },
+ },
+});
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/apps/web/src/pages/About.tsx b/apps/web/src/pages/About.tsx
new file mode 100644
index 0000000..686ae7b
--- /dev/null
+++ b/apps/web/src/pages/About.tsx
@@ -0,0 +1,234 @@
+import type { ReactNode } from 'react';
+import { APP_VERSION } from '@vectora/shared';
+import { SitePage } from '@/components/Site';
+import { useHealth } from '@/lib/api';
+
+const CREDITS: [string, string, string][] = [
+ ['TypeScript', 'Compiler API used for TypeScript and JavaScript analysis', 'Apache-2.0'],
+ ['Tree-sitter', 'Parsing for Java, Go, Rust, C, C++ and C#', 'MIT'],
+ ['Python', 'Standard-library ast module for Python analysis', 'PSF'],
+ ['SQLite', 'Local graph database (built into Node.js)', 'Public domain'],
+ ['Fastify', 'Local engine HTTP server', 'MIT'],
+ ['React and Vite', 'Application framework and build tool', 'MIT'],
+ ['Sigma.js and Graphology', 'Two-dimensional graph rendering and graph algorithms', 'MIT'],
+ ['Three.js, React Three Fiber and drei', 'Three-dimensional rendering', 'MIT'],
+ ['d3-force-3d', 'Force simulation for the 3D layout', 'MIT'],
+ ['React Flow and dagre', 'Mind map canvas and hierarchical layout', 'MIT'],
+ ['CodeMirror 6', 'Source preview', 'MIT'],
+ ['Radix UI, cmdk and Lucide', 'Accessible dialogs, command palette and icons', 'MIT / ISC'],
+ ['Tailwind CSS', 'Styling', 'MIT'],
+ [
+ 'Zen Kaku Gothic New, Shippori Mincho, IBM Plex Mono',
+ 'Typefaces, bundled with the application',
+ 'SIL OFL 1.1',
+ ],
+];
+
+function Block({ id, title, children }: { id: string; title: string; children: ReactNode }) {
+ return (
+
+
+ {title}
+
+
+ {children}
+
+
+ );
+}
+
+export function About() {
+ const health = useHealth();
+ return (
+
+
+
+
+
+
+ Software is understood through its connections: what calls what, what depends on what, where a
+ request ends up. Those connections are spread across hundreds of files, and reading the files in
+ order does not reveal them.
+
+ VECTORA is built on three commitments.
+
+ Evidence first. Every relationship carries the file, the
+ line and the method that found it. A reference that cannot be tied to a definition is reported as
+ unresolved. It is never drawn as if it were known.
+
+
+ Local by default. Analysis, storage and search run on
+ your own machine. The application works with no account and no hosted service.
+
+
+ Restraint. The interface follows the Japanese ideas of
+ kanso, simplicity, and ma, the meaningful interval. Space, type and a small palette do the work;
+ decoration does not.
+
+
+
+
+
+ A repository goes through a fixed pipeline: discovery of files and manifests, syntax analysis by a
+ language adapter, resolution of references across files, tracing of execution flows, grouping into
+ subsystems and concepts, and generation of a validated knowledge graph that is stored in a local
+ database.
+
+
+ The Graph Explorer, the 3D Atlas and the Mind Map all read that one graph and share one selection.
+ Verified structure and derived interpretation are kept apart throughout, and drawn differently.
+
+
+
+
+
+ Symbol-level analysis is available for the languages below. Other files are indexed as files only,
+ and each repository shows its own language coverage.
+
+
+
+
+ Language
+ Tier
+ Technique
+
+
+
+ {(health.data?.languages ?? FALLBACK_LANGUAGES).map((l) => (
+
+ {l.name}
+ {l.tier}
+
+ {l.id === 'typescript' || l.id === 'javascript'
+ ? 'TypeScript Compiler API with type-checked resolution'
+ : l.id === 'python'
+ ? 'Python ast module with import and scope resolution'
+ : 'Tree-sitter syntax tree with import and scope resolution'}
+
+
+ ))}
+
+
+
+ Tier 1 languages have the most precise relationship resolution. VECTORA does not claim to support
+ every language.
+
+
+
+
+
+ Source code is read and analysed locally. It is not uploaded.
+ Repository scripts are never executed and repository dependencies are never installed.
+
+ Access tokens are stored encrypted on your machine, sent only to GitHub in a request header, and
+ never written to logs, URLs or exports.
+
+
+ Exports contain names, paths and line numbers. Lines of source are included only when you ask
+ for them.
+
+
+ The one optional external service, a narrative summary for answers, is off by default and
+ receives only the findings already shown on screen.
+
+ All stored data can be deleted from Settings.
+
+
+
+
+ VECTORA stands on open-source work. With thanks to the people who maintain it.
+
+
+
+ Project
+ Used for
+ Licence
+
+
+
+ {CREDITS.map(([name, use, lic]) => (
+
+ {name}
+ {use}
+ {lic}
+
+ ))}
+
+
+
+ The full inventory of dependency licences is generated into docs/LICENSES.md in the
+ VECTORA folder.
+
+
+
+
+
+ Version
+ {APP_VERSION}
+ Local engine
+
+ {health.data
+ ? `running, version ${health.data.version} on Node.js ${health.data.node}`
+ : health.isLoading
+ ? 'checking'
+ : 'not reachable'}
+
+ Licence
+ MIT. See the LICENSE file in the VECTORA folder.
+
+
+
+
+
+ The documentation ships with the application, in the docs folder:
+
+
+ {[
+ 'USER_GUIDE.md',
+ 'ARCHITECTURE.md',
+ 'DATA_MODEL.md',
+ 'INDEXING_PIPELINE.md',
+ 'VISUALIZATION.md',
+ 'SECURITY.md',
+ 'PERFORMANCE.md',
+ 'TROUBLESHOOTING.md',
+ ].map((f) => (
+ docs/{f}
+ ))}
+
+
+ Changes between versions are recorded in docs/CHANGELOG.md.
+
+
+
+
+ );
+}
+
+const FALLBACK_LANGUAGES = [
+ { id: 'typescript', name: 'TypeScript', tier: 1 },
+ { id: 'javascript', name: 'JavaScript', tier: 1 },
+ { id: 'python', name: 'Python', tier: 1 },
+ { id: 'java', name: 'Java', tier: 2 },
+ { id: 'go', name: 'Go', tier: 2 },
+ { id: 'rust', name: 'Rust', tier: 2 },
+ { id: 'c', name: 'C', tier: 2 },
+ { id: 'cpp', name: 'C++', tier: 2 },
+ { id: 'csharp', name: 'C#', tier: 2 },
+];
diff --git a/apps/web/src/pages/ImportPage.tsx b/apps/web/src/pages/ImportPage.tsx
new file mode 100644
index 0000000..000f92b
--- /dev/null
+++ b/apps/web/src/pages/ImportPage.tsx
@@ -0,0 +1,600 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { Folder, FolderGit2 } from 'lucide-react';
+import { useEffect, useRef, useState } from 'react';
+import { useNavigate } from 'react-router';
+import { formatBytes } from '@vectora/shared';
+import { SitePage } from '@/components/Site';
+import { Dialog, Notice, cx } from '@/components/ui';
+import {
+ ApiError,
+ type GitHubInfo,
+ type ZipInspection,
+ api,
+ useHealth,
+ useJob,
+ useRepositories,
+} from '@/lib/api';
+import { IndexingProgress } from '@/workspace/WorkspaceLayout';
+
+type Method = 'github' | 'zip' | 'local';
+
+export function ImportPage() {
+ const navigate = useNavigate();
+ const qc = useQueryClient();
+ const health = useHealth();
+ const repos = useRepositories();
+ const [method, setMethod] = useState('github');
+ const [jobId, setJobId] = useState(null);
+ const [error, setError] = useState(null);
+ const job = useJob(jobId).data;
+
+ useEffect(() => {
+ if (!job || job.status === 'running') return;
+ void qc.invalidateQueries({ queryKey: ['repositories'] });
+ if (job.status === 'completed' && job.repositoryId) navigate(`/w/${job.repositoryId}`);
+ else if (job.status === 'failed') {
+ setError(job.error ?? 'Indexing failed');
+ setJobId(null);
+ }
+ }, [job, navigate, qc]);
+
+ const start = async (fn: () => Promise<{ id: string }>) => {
+ setError(null);
+ try {
+ setJobId((await fn()).id);
+ } catch (e) {
+ setError((e as Error).message);
+ }
+ };
+ const offline = health.error && (health.error as ApiError).status === 0;
+
+ if (jobId)
+ return (
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
+ Explore a repository
+
+
+ The source is analysed on this machine. Nothing in the repository is installed or run.
+
+
+ {offline ? (
+
+
+ The local engine is not running, so repositories cannot be imported. Start it with{' '}
+ npm start in the VECTORA folder and reload this page.
+
+
+ ) : null}
+ {error ? (
+
+ setError(null)}>
+ Dismiss
+
+ }
+ >
+ {error}
+
+
+ ) : null}
+
+
+ {(
+ [
+ ['github', 'GitHub repository'],
+ ['zip', 'ZIP archive'],
+ ['local', 'Local folder'],
+ ] as const
+ ).map(([id, label]) => (
+ setMethod(id)}
+ >
+ {label}
+
+ ))}
+
+
+ {method === 'github' ? (
+
+ ) : method === 'zip' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Your repositories
+
+ {repos.isLoading ? Loading…
: null}
+ {repos.data?.length === 0 ? (
+
+ Nothing imported yet. Repositories you index appear here and reopen with their saved workspace.
+
+ ) : null}
+
+ {repos.data?.map((r) => (
+
+ navigate(`/w/${r.id}`)}
+ data-testid="repo-link"
+ >
+ {r.name}
+
+ {r.sourceKind}
+ {r.branch ? `, ${r.branch}` : ''} —{' '}
+ {r.status === 'ready'
+ ? `${r.symbolCount.toLocaleString()} symbols, ${r.relationshipCount.toLocaleString()} relationships`
+ : r.status}
+
+
+ {
+ if (
+ !window.confirm(
+ `Remove “${r.name}” from VECTORA? Its graph and saved workspace are deleted.${r.sourceKind === 'local' || r.sourceKind === 'sample' ? ' The folder itself is not touched.' : ''}`,
+ )
+ )
+ return;
+ await api.deleteRepository(r.id);
+ void qc.invalidateQueries({ queryKey: ['repositories'] });
+ }}
+ >
+ Remove
+
+
+ ))}
+
+ {health.data?.sampleAvailable ? (
+
+
New here? Try the bundled sample.
+
+ “Orbit Notes” is a small, clearly labelled multi-language repository included for onboarding
+ and tests. It is not a real product.
+
+
void start(api.importSample)}
+ data-testid="import-sample"
+ >
+ Index the sample repository
+
+
+ ) : null}
+
+
+
+ );
+}
+
+type Start = (fn: () => Promise<{ id: string }>) => Promise;
+
+function GitHubForm({ onStart }: { onStart: Start }) {
+ const [url, setUrl] = useState('');
+ const [token, setToken] = useState('');
+ const [remember, setRemember] = useState(false);
+ const [info, setInfo] = useState(null);
+ const [branch, setBranch] = useState('');
+ const [subdir, setSubdir] = useState('');
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState(null);
+ const validate = async () => {
+ setBusy(true);
+ setError(null);
+ setInfo(null);
+ try {
+ const i = await api.validateGitHub(url, token);
+ setInfo(i);
+ setBranch(
+ i.requested.branch && i.branches.includes(i.requested.branch) ? i.requested.branch : i.defaultBranch,
+ );
+ setSubdir(i.requested.subdirectory ?? '');
+ } catch (e) {
+ setError((e as Error).message);
+ } finally {
+ setBusy(false);
+ }
+ };
+ return (
+
+ );
+}
+
+function ZipForm({ onStart }: { onStart: Start }) {
+ const input = useRef(null);
+ const [inspection, setInspection] = useState(null);
+ const [root, setRoot] = useState('');
+ const [busy, setBusy] = useState(false);
+ const [error, setError] = useState(null);
+ const [over, setOver] = useState(false);
+ const upload = async (file: File | undefined) => {
+ if (!file) return;
+ setError(null);
+ setInspection(null);
+ if (!/\.zip$/i.test(file.name)) return setError('Choose a file that ends in .zip.');
+ setBusy(true);
+ try {
+ const r = await api.inspectZip(file);
+ setInspection(r);
+ setRoot(r.roots[0]?.path ?? '');
+ } catch (e) {
+ setError((e as Error).message);
+ } finally {
+ setBusy(false);
+ }
+ };
+ return (
+
+
{
+ e.preventDefault();
+ setOver(true);
+ }}
+ onDragLeave={() => setOver(false)}
+ onDrop={(e) => {
+ e.preventDefault();
+ setOver(false);
+ void upload(e.dataTransfer.files[0]);
+ }}
+ className={cx(
+ 'border border-dashed px-6 py-10 text-center transition-colors duration-200',
+ over ? 'border-indigo bg-surface' : 'border-ash',
+ )}
+ >
+
Drop a ZIP archive here
+
up to 512 MB. The archive is checked before anything is extracted.
+
void upload(e.target.files?.[0])}
+ />
+
+ {busy ? 'Checking archive' : 'Choose a file'}
+
+
+ {error ?
{error} : null}
+ {inspection ? (
+
+
+
{inspection.fileName}
+
+ {inspection.files.toLocaleString()} files, {formatBytes(inspection.totalBytes)} uncompressed
+
+
+ {inspection.rejected.length ? (
+
+ {inspection.rejected.length} entr{inspection.rejected.length === 1 ? 'y' : 'ies'} will be
+ skipped as unsafe ({[...new Set(inspection.rejected.map((r) => r.reason))].join(', ')}).
+
+ ) : null}
+
+ Project root
+ {inspection.roots.map((r) => (
+
+ setRoot(r.path)}
+ />
+ {r.path || '(archive root)'}
+
+ {r.files.toLocaleString()} files{r.markers.length ? ` — ${r.markers.join(', ')}` : ''}
+
+
+ ))}
+
+
+ Detected structure
+
+ {inspection.tree.slice(0, 200).map((t) => (
+
+ {t.path.split('/').pop()}
+ {t.directory ? '/' : ''}
+
+ ))}
+
+
+
+ void onStart(() => api.importZip(inspection.uploadId, root))}
+ >
+ Extract and index
+
+
+
+ ) : null}
+
+ Ignored folders such as node_modules, binary files, symbolic links and entries that point outside the
+ archive are never extracted.
+
+
+ );
+}
+
+function LocalForm({ onStart }: { onStart: Start }) {
+ const [path, setPath] = useState('');
+ const [browse, setBrowse] = useState(false);
+ return (
+
+ );
+}
+
+function FolderBrowser({
+ open,
+ onOpenChange,
+ initial,
+ onPick,
+}: {
+ open: boolean;
+ onOpenChange: (o: boolean) => void;
+ initial: string;
+ onPick: (p: string) => void;
+}) {
+ const [dir, setDir] = useState(initial || undefined);
+ const list = useQuery({
+ queryKey: ['fs', dir],
+ queryFn: () => api.listDir(dir),
+ enabled: open,
+ retry: false,
+ });
+ return (
+
+
+ {list.data?.roots.map((r) => (
+ setDir(r)}>
+ {r}
+
+ ))}
+ {list.data?.parent ? (
+ setDir(list.data!.parent!)}>
+ Up one level
+
+ ) : null}
+
+ {list.data?.path ?? dir ?? ''}
+ {list.error ? (
+
+ setDir(undefined)}>
+ Start over
+
+ }
+ >
+ {(list.error as Error).message}
+
+
+ ) : null}
+
+ {list.data?.entries.length === 0 ? No folders here. : null}
+ {list.data?.entries.map((e) => (
+
+ setDir(e.path)}
+ onClick={() => setDir(e.path)}
+ >
+ {e.repository ? (
+
+ ) : (
+
+ )}
+ {e.name}
+ {e.repository ? looks like a project : null}
+
+
+ ))}
+
+
+ onOpenChange(false)}>
+ Cancel
+
+ {
+ if (list.data) onPick(list.data.path);
+ onOpenChange(false);
+ }}
+ >
+ Use this folder
+
+
+
+ );
+}
diff --git a/apps/web/src/pages/Landing.tsx b/apps/web/src/pages/Landing.tsx
new file mode 100644
index 0000000..41f018c
--- /dev/null
+++ b/apps/web/src/pages/Landing.tsx
@@ -0,0 +1,259 @@
+import { Link } from 'react-router';
+import { AtlasIllustration, SitePage } from '@/components/Site';
+
+const STEPS = [
+ {
+ mark: '一',
+ title: 'Import',
+ body: 'Point VECTORA at a GitHub repository, a ZIP archive or a folder on your machine. Ignore rules are respected and nothing in the repository is ever run.',
+ },
+ {
+ mark: '二',
+ title: 'Index',
+ body: 'Source files are parsed into syntax trees. Classes, functions, imports, routes and data models are extracted with the file and line they came from.',
+ },
+ {
+ mark: '三',
+ title: 'Resolve',
+ body: 'References are tied to definitions only when there is scope evidence for it. What cannot be resolved is reported, not guessed.',
+ },
+ {
+ mark: '四',
+ title: 'Explore',
+ body: 'The result is one knowledge graph, shown three ways. Select a node anywhere and the other views follow.',
+ },
+];
+
+const VIEWS = [
+ {
+ name: 'Graph Explorer',
+ dimension: 'Two dimensions',
+ body: 'Pan and zoom through files and symbols. Selecting a node highlights what it depends on and what depends on it, and dims the rest.',
+ },
+ {
+ name: '3D Atlas',
+ dimension: 'Three dimensions',
+ body: 'The repository at the centre, subsystems as regions around it, files and symbols inside their regions. Fly to a node, unfold it, return to the whole.',
+ },
+ {
+ name: 'Mind Map',
+ dimension: 'Hierarchy',
+ body: 'Subsystems, folders, files and symbols as a tree you can expand, rearrange and export. Concepts sit beside the structure they were found in.',
+ },
+];
+
+const LANGUAGES: [string, string][] = [
+ ['TypeScript and JavaScript', 'type-checked symbol resolution'],
+ ['Python', 'syntax-tree analysis'],
+ ['Java, Go, Rust, C, C++, C#', 'syntax-tree analysis'],
+];
+
+export function Landing() {
+ return (
+
+ {/* Hero — text held to a narrow left column, the drawing bleeding off the right edge */}
+
+
+ {'VECTORA 構造を読む'}
+
+
+
+ Understand what connects.
+
+
+ Explore the hidden architecture of software. Transform repositories into structured, interactive
+ knowledge maps.
+
+
+
+ Explore a repository
+
+
+ Learn about VECTORA
+
+
+
+
+
+
+ Illustration of the 3D Atlas: a repository, its subsystems and their contents.
+
+
+
+
+
+
+ {/* Product visualisation */}
+
+
+ A repository is a set of relationships. Reading files one by one hides them.
+
+
+
+ VECTORA reads the source, records which function calls which, which class extends which, which
+ route leads to which handler, and keeps the line of code that proves it.
+
+
+ Every relationship in the graph can be opened to show its evidence. Groupings that are
+ interpretations, such as concepts, are marked as derived and drawn differently from verified
+ structure.
+
+
+
+
+ {/* How it works — a real sequence, so it is numbered */}
+
+
+
+ How it works
+
+
+ {STEPS.map((s, i) => (
+
+
+ {s.mark}
+
+ Step {i + 1}:
+ {s.title}
+ {s.body}
+
+ ))}
+
+
+
+
+ {/* Exploration capabilities */}
+
+
+
+ One graph, three ways to read it
+
+
+ {VIEWS.map((v) => (
+
+ {v.dimension}
+
+
+ ))}
+
+ Around them: an inspector with the original source, search across files, symbols and
+ documentation, a dependency explorer with circular imports and unresolved references, and
+ questions answered with file and line citations.
+
+
+
+
+
+ {/* Supported source formats */}
+
+
+
+
+
+ {[
+ [
+ 'GitHub repository',
+ 'Public repositories by URL. Private repositories with an access token you provide; it is stored encrypted on your machine.',
+ ],
+ [
+ 'ZIP archive',
+ 'Checked before extraction. Nested project folders are detected and you choose the root.',
+ ],
+ [
+ 'Local folder',
+ 'Read in place through the local engine. .gitignore and your own exclusion rules apply.',
+ ],
+ ].map(([k, v]) => (
+
+
{k}
+ {v}
+
+ ))}
+
+
+
+
Languages analysed
+
+ {LANGUAGES.map(([k, v]) => (
+
+ {k}
+ {v}
+
+ ))}
+
+
+ Files in other languages are listed and searchable but not analysed at symbol level. Language
+ coverage is shown for every repository.
+
+
+
+
+
+ {/* Privacy */}
+
+
+
+ Your code stays on your machine
+
+
+
+ Indexing, the graph database and search all run in a local engine. No source code is uploaded.
+
+ Repository dependencies are never installed and repository scripts are never executed.
+
+ The only network requests are the ones you start: downloading a repository from GitHub, and an
+ optional narrative service that is off by default.
+
+
+ Exports leave out source text unless you ask for it, and everything can be deleted from
+ Settings.
+
+
+
+
+
+ {/* About */}
+
+
+ About
+
+
+
+ VECTORA is a codebase intelligence platform that transforms software repositories into structured,
+ interactive knowledge maps. It reveals how components, modules, dependencies and concepts connect,
+ giving developers a new way to understand complex software systems.
+
+
+ Philosophy, supported languages, credits and licence
+
+
+
+
+ );
+}
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
new file mode 100644
index 0000000..5820ee0
--- /dev/null
+++ b/apps/web/src/styles.css
@@ -0,0 +1,530 @@
+/* Latin subsets only: the three faces are bundled, nothing is fetched from a font service.
+ The few Japanese glyphs used as marks fall back to the system mincho / gothic. */
+@import '@fontsource/zen-kaku-gothic-new/latin-400.css';
+@import '@fontsource/zen-kaku-gothic-new/latin-500.css';
+@import '@fontsource/zen-kaku-gothic-new/latin-700.css';
+@import '@fontsource/zen-kaku-gothic-new/latin-ext-400.css';
+@import '@fontsource/shippori-mincho/latin-400.css';
+@import '@fontsource/shippori-mincho/latin-600.css';
+@import '@fontsource/shippori-mincho/latin-ext-400.css';
+@import '@fontsource/ibm-plex-mono/latin-400.css';
+@import '@fontsource/ibm-plex-mono/latin-500.css';
+@import 'tailwindcss';
+
+/* ---------------------------------------------------------------------------
+ Tokens. Values mirror src/lib/tokens.ts (checked by a unit test).
+ --------------------------------------------------------------------------- */
+:root {
+ --canvas: #f8f7f2;
+ --surface: #f3f1ea;
+ --elevated: #fbfaf6;
+ --text: #20211f;
+ --text-secondary: #5f5d55;
+ --text-muted: #8c897f;
+ --line: #dcd8ce;
+ --indigo: #34445c;
+ --clay: #a18c72;
+ --ash: #b8b3a7;
+ --success: #55704f;
+ --warning: #7d5c1b;
+ --error: #964436;
+ --selection: #34445c;
+ --relation-active: #34445c;
+ --relation-unverified: #a18c72;
+ --on-accent: #f8f7f2;
+ --duration-ui: 200ms;
+ --duration-panel: 380ms;
+ --ease: cubic-bezier(0.2, 0, 0, 1);
+ color-scheme: light;
+}
+
+:root[data-theme='dark'] {
+ --canvas: #171816;
+ --surface: #20211f;
+ --elevated: #272824;
+ --text: #eceae2;
+ --text-secondary: #a7a499;
+ --text-muted: #77766d;
+ --line: #373832;
+ --indigo: #8292ad;
+ --clay: #b39a7a;
+ --ash: #5c5b54;
+ --success: #8fae8a;
+ --warning: #c9a566;
+ --error: #d08c7c;
+ --selection: #8292ad;
+ --relation-active: #8292ad;
+ --relation-unverified: #b39a7a;
+ --on-accent: #171816;
+ color-scheme: dark;
+}
+
+:root[data-motion='reduced'] {
+ --duration-ui: 0ms;
+ --duration-panel: 0ms;
+}
+
+/* Reduced motion. Kept outside the cascade layers so it overrides the token values above. */
+@media (prefers-reduced-motion: reduce) {
+ :root:not([data-motion='full']) {
+ --duration-ui: 0ms;
+ --duration-panel: 0ms;
+ }
+ :root:not([data-motion='full']) *,
+ :root:not([data-motion='full']) *::before,
+ :root:not([data-motion='full']) *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ scroll-behavior: auto !important;
+ }
+}
+:root[data-motion='reduced'] *,
+:root[data-motion='reduced'] *::before,
+:root[data-motion='reduced'] *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0ms !important;
+}
+
+@theme inline {
+ --color-canvas: var(--canvas);
+ --color-surface: var(--surface);
+ --color-elevated: var(--elevated);
+ --color-ink: var(--text);
+ --color-ink-2: var(--text-secondary);
+ --color-ink-3: var(--text-muted);
+ --color-line: var(--line);
+ --color-indigo: var(--indigo);
+ --color-clay: var(--clay);
+ --color-ash: var(--ash);
+ --color-success: var(--success);
+ --color-warning: var(--warning);
+ --color-error: var(--error);
+ --color-on-accent: var(--on-accent);
+ --font-sans: 'Zen Kaku Gothic New', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', system-ui, sans-serif;
+ --font-serif: 'Shippori Mincho', 'Hiragino Mincho ProN', 'Yu Mincho', serif;
+ --font-mono: 'IBM Plex Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
+ /* Type scale */
+ --text-meta: 0.75rem;
+ --text-label: 0.8125rem;
+ --text-body: 0.9375rem;
+ --text-section: 1.375rem;
+ --text-page: clamp(1.875rem, 1.4rem + 1.6vw, 2.5rem);
+ --text-display: clamp(2.75rem, 1.6rem + 4.2vw, 4rem);
+ --radius-sm: 2px;
+ --radius-md: 3px;
+}
+
+@layer base {
+ html {
+ background: var(--canvas);
+ color: var(--text);
+ font-family: var(--font-sans);
+ font-size: 16px;
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ }
+ body {
+ margin: 0;
+ min-height: 100dvh;
+ font-size: var(--text-body);
+ font-feature-settings: 'palt' 1;
+ }
+ #root {
+ min-height: 100dvh;
+ }
+ h1,
+ h2,
+ h3,
+ h4 {
+ font-weight: 500;
+ line-height: 1.25;
+ margin: 0;
+ text-wrap: balance;
+ }
+ p {
+ margin: 0;
+ text-wrap: pretty;
+ }
+ a {
+ color: inherit;
+ text-decoration-color: var(--line);
+ text-underline-offset: 0.22em;
+ text-decoration-thickness: 1px;
+ transition:
+ text-decoration-color var(--duration-ui) var(--ease),
+ color var(--duration-ui) var(--ease);
+ }
+ a:hover {
+ text-decoration-color: currentColor;
+ }
+ code,
+ kbd,
+ pre,
+ samp {
+ font-family: var(--font-mono);
+ font-size: 0.86em;
+ }
+ ::selection {
+ background: color-mix(in srgb, var(--indigo) 24%, transparent);
+ }
+ :focus-visible {
+ outline: 2px solid var(--indigo);
+ outline-offset: 2px;
+ }
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--line) transparent;
+ }
+}
+
+@layer components {
+ .font-editorial {
+ font-family: var(--font-serif);
+ font-weight: 400;
+ letter-spacing: 0.01em;
+ }
+ .meta {
+ font-family: var(--font-mono);
+ font-size: var(--text-meta);
+ color: var(--text-muted);
+ letter-spacing: 0;
+ }
+ .label {
+ font-size: var(--text-label);
+ color: var(--text-secondary);
+ }
+ .rule {
+ border: 0;
+ border-top: 1px solid var(--line);
+ margin: 0;
+ }
+ /* Vertical brand spine — Japanese vertical setting used as the signature element. */
+ .spine {
+ writing-mode: vertical-rl;
+ text-orientation: mixed;
+ letter-spacing: 0.42em;
+ font-size: var(--text-meta);
+ color: var(--text-muted);
+ }
+ .btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ min-height: 36px;
+ padding: 0 16px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: var(--text);
+ font: inherit;
+ font-size: var(--text-label);
+ font-weight: 500;
+ line-height: 1;
+ cursor: pointer;
+ text-decoration: none;
+ white-space: nowrap;
+ transition:
+ background var(--duration-ui) var(--ease),
+ border-color var(--duration-ui) var(--ease),
+ color var(--duration-ui) var(--ease);
+ }
+ .btn:hover:not(:disabled) {
+ border-color: var(--text-muted);
+ background: color-mix(in srgb, var(--text) 4%, transparent);
+ }
+ .btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+ .btn-primary {
+ background: var(--text);
+ border-color: var(--text);
+ color: var(--canvas);
+ }
+ .btn-primary:hover:not(:disabled) {
+ background: var(--indigo);
+ border-color: var(--indigo);
+ color: var(--on-accent);
+ }
+ .btn-quiet {
+ border-color: transparent;
+ padding: 0 8px;
+ color: var(--text-secondary);
+ }
+ .btn-sm {
+ min-height: 28px;
+ padding: 0 10px;
+ font-size: var(--text-meta);
+ }
+ .btn-icon {
+ min-height: 32px;
+ width: 32px;
+ padding: 0;
+ }
+ .btn[aria-pressed='true'],
+ .btn[data-active='true'] {
+ border-color: var(--text);
+ background: color-mix(in srgb, var(--text) 6%, transparent);
+ }
+ .field {
+ width: 100%;
+ min-height: 36px;
+ padding: 6px 12px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ background: var(--elevated);
+ color: var(--text);
+ font: inherit;
+ font-size: var(--text-label);
+ transition: border-color var(--duration-ui) var(--ease);
+ }
+ .field::placeholder {
+ color: var(--text-muted);
+ }
+ .field:hover {
+ border-color: var(--ash);
+ }
+ .field:focus-visible {
+ outline: none;
+ border-color: var(--indigo);
+ box-shadow: 0 0 0 1px var(--indigo);
+ }
+ textarea.field {
+ font-family: var(--font-mono);
+ font-size: var(--text-meta);
+ line-height: 1.7;
+ resize: vertical;
+ }
+ .panel {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ }
+ .tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 1px 8px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ font-size: var(--text-meta);
+ color: var(--text-secondary);
+ line-height: 1.7;
+ white-space: nowrap;
+ }
+ .tag-derived {
+ border-style: dashed;
+ border-color: var(--relation-unverified);
+ color: var(--text-secondary);
+ }
+ .link-row {
+ display: flex;
+ width: 100%;
+ align-items: baseline;
+ gap: 12px;
+ padding: 8px 0;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ background: none;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ text-decoration: none;
+ transition:
+ padding-left var(--duration-ui) var(--ease),
+ color var(--duration-ui) var(--ease);
+ }
+ .link-row:hover {
+ padding-left: 6px;
+ color: var(--indigo);
+ }
+ .prose-doc {
+ max-width: 68ch;
+ line-height: 1.75;
+ }
+ .prose-doc h1,
+ .prose-doc h2,
+ .prose-doc h3 {
+ font-family: var(--font-serif);
+ margin: 1.8em 0 0.6em;
+ }
+ .prose-doc h1 {
+ font-size: 1.75rem;
+ }
+ .prose-doc h2 {
+ font-size: 1.3rem;
+ }
+ .prose-doc h3 {
+ font-size: 1.1rem;
+ }
+ .prose-doc p,
+ .prose-doc ul,
+ .prose-doc ol,
+ .prose-doc pre,
+ .prose-doc table,
+ .prose-doc blockquote {
+ margin: 0 0 1em;
+ }
+ .prose-doc ul,
+ .prose-doc ol {
+ padding-left: 1.3em;
+ }
+ .prose-doc pre {
+ padding: 12px 16px;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ overflow-x: auto;
+ font-size: var(--text-meta);
+ line-height: 1.7;
+ }
+ .prose-doc :not(pre) > code {
+ padding: 0.1em 0.35em;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ }
+ .prose-doc blockquote {
+ padding-left: 16px;
+ border-left: 2px solid var(--clay);
+ color: var(--text-secondary);
+ }
+ .prose-doc table {
+ border-collapse: collapse;
+ font-size: var(--text-label);
+ display: block;
+ overflow-x: auto;
+ }
+ .prose-doc th,
+ .prose-doc td {
+ padding: 6px 12px;
+ border-bottom: 1px solid var(--line);
+ text-align: left;
+ }
+ .prose-doc img {
+ max-width: 100%;
+ }
+ .data-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: var(--text-label);
+ }
+ .data-table th {
+ font-weight: 500;
+ color: var(--text-muted);
+ text-align: left;
+ padding: 6px 12px 6px 0;
+ border-bottom: 1px solid var(--line);
+ white-space: nowrap;
+ }
+ .data-table td {
+ padding: 7px 12px 7px 0;
+ border-bottom: 1px solid var(--line);
+ vertical-align: baseline;
+ }
+ .data-table .num {
+ text-align: right;
+ font-family: var(--font-mono);
+ font-size: var(--text-meta);
+ font-variant-numeric: tabular-nums;
+ }
+ .skip-link {
+ position: absolute;
+ left: 8px;
+ top: -48px;
+ z-index: 100;
+ padding: 8px 12px;
+ background: var(--text);
+ color: var(--canvas);
+ }
+ .skip-link:focus {
+ top: 8px;
+ }
+}
+
+/* Overlays */
+@keyframes overlay-in {
+ from {
+ opacity: 0;
+ }
+}
+@keyframes panel-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ }
+}
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ background: color-mix(in srgb, var(--text) 22%, transparent);
+ animation: overlay-in var(--duration-ui) var(--ease);
+}
+:root[data-theme='dark'] .overlay {
+ background: rgba(0, 0, 0, 0.55);
+}
+.dialog {
+ position: fixed;
+ z-index: 51;
+ left: 50%;
+ top: 14vh;
+ width: min(640px, calc(100vw - 32px));
+ max-height: 72vh;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ background: var(--elevated);
+ border: 1px solid var(--line);
+ box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.45);
+ animation: panel-in var(--duration-panel) var(--ease);
+}
+
+/* Library overrides: React Flow and CodeMirror follow the tokens. */
+.react-flow {
+ --xy-background-color: transparent;
+ --xy-edge-stroke: var(--ash);
+ --xy-edge-stroke-selected: var(--indigo);
+ --xy-controls-button-background-color: var(--elevated);
+ --xy-controls-button-background-color-hover: var(--surface);
+ --xy-controls-button-color: var(--text);
+ --xy-controls-button-border-color: var(--line);
+ --xy-minimap-background-color: var(--surface);
+ --xy-minimap-mask-background-color: color-mix(in srgb, var(--canvas) 70%, transparent);
+ --xy-attribution-background-color: transparent;
+ font-family: var(--font-sans);
+}
+.react-flow__controls {
+ box-shadow: none;
+ border: 1px solid var(--line);
+}
+.react-flow__attribution a {
+ color: var(--text-muted);
+}
+.cm-editor {
+ font-size: 12.5px;
+ background: transparent;
+}
+.cm-editor .cm-scroller {
+ font-family: var(--font-mono);
+ line-height: 1.65;
+}
+.cm-editor .cm-gutters {
+ background: transparent;
+ color: var(--text-muted);
+ border-right: 1px solid var(--line);
+}
+.cm-editor.cm-focused {
+ outline: none;
+}
+.cm-editor .cm-activeLine,
+.cm-editor .cm-activeLineGutter {
+ background: transparent;
+}
+.cm-editor .cm-vectora-range {
+ background: color-mix(in srgb, var(--indigo) 9%, transparent);
+}
+.sigma-container canvas {
+ outline: none;
+}
diff --git a/apps/web/src/theme-init.ts b/apps/web/src/theme-init.ts
new file mode 100644
index 0000000..182fd32
--- /dev/null
+++ b/apps/web/src/theme-init.ts
@@ -0,0 +1,5 @@
+// Applies the stored theme before the application renders, so there is no flash of the wrong theme.
+const stored = localStorage.getItem('vectora-theme');
+const dark =
+ stored === 'dark' || (stored !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
+document.documentElement.dataset.theme = dark ? 'dark' : 'light';
diff --git a/apps/web/src/workers/layout3d.worker.ts b/apps/web/src/workers/layout3d.worker.ts
new file mode 100644
index 0000000..6cd9197
--- /dev/null
+++ b/apps/web/src/workers/layout3d.worker.ts
@@ -0,0 +1,23 @@
+///
+import { type LayoutEdge, type LayoutNode, layout3d } from '@vectora/graph-layout';
+
+export interface Layout3DRequest {
+ requestId: number;
+ nodes: LayoutNode[];
+ edges: LayoutEdge[];
+ quality: number;
+}
+export interface Layout3DResponse {
+ requestId: number;
+ positions: Float32Array;
+ radius: number;
+}
+
+self.onmessage = (e: MessageEvent) => {
+ const { requestId, nodes, edges, quality } = e.data;
+ const result = layout3d(nodes, edges, { quality });
+ (self as unknown as Worker).postMessage(
+ { requestId, positions: result.positions, radius: result.radius } satisfies Layout3DResponse,
+ [result.positions.buffer],
+ );
+};
diff --git a/apps/web/src/workspace/Atlas.tsx b/apps/web/src/workspace/Atlas.tsx
new file mode 100644
index 0000000..bd3cea1
--- /dev/null
+++ b/apps/web/src/workspace/Atlas.tsx
@@ -0,0 +1,899 @@
+import { OrbitControls } from '@react-three/drei';
+import { Canvas, type ThreeEvent, useFrame, useThree } from '@react-three/fiber';
+import { ChevronRight, Globe, Image as ImageIcon, RotateCcw } from 'lucide-react';
+import {
+ type MutableRefObject,
+ useCallback,
+ useEffect,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import * as THREE from 'three';
+import type { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
+import type { LayoutNode, LayoutRole } from '@vectora/graph-layout';
+import { type GraphEdge, type GraphNode, NODE_TYPE_LABELS, isSymbolType } from '@vectora/graph-schema';
+import { ViewToolbar } from '@/components/ViewControls';
+import { Loading } from '@/components/ui';
+import { downloadUrl } from '@/lib/api';
+import { fileIdOf, neighbourhood } from '@/lib/graphView';
+import { useWorkspace } from '@/lib/store';
+import {
+ FAMILY_GLYPH,
+ NODE_FAMILY,
+ NODE_SIZE,
+ type NodeFamily,
+ type Palette,
+ mix,
+ nodeColor,
+} from '@/lib/tokens';
+import type { Layout3DRequest, Layout3DResponse } from '@/workers/layout3d.worker';
+import { useWs } from './context';
+
+type Quality = 'low' | 'balanced' | 'high';
+
+interface AtlasApi {
+ reset: () => void;
+ focusOn: (id: string) => void;
+ capture: () => string | null;
+}
+
+const roleOf = (n: GraphNode): LayoutRole => {
+ switch (n.type) {
+ case 'Repository':
+ return 'repository';
+ case 'Subsystem':
+ return 'subsystem';
+ case 'Concept':
+ return 'concept';
+ case 'Package':
+ return 'package';
+ case 'Directory':
+ case 'Module':
+ return 'container';
+ case 'File':
+ case 'Configuration':
+ return 'file';
+ default:
+ return 'symbol';
+ }
+};
+
+function webglAvailable(): boolean {
+ try {
+ const c = document.createElement('canvas');
+ return !!(c.getContext('webgl2') ?? c.getContext('webgl'));
+ } catch {
+ return false;
+ }
+}
+
+/** Runs the 3D layout in a Web Worker and returns positions aligned with `nodes`. */
+function useLayout3d(
+ nodes: GraphNode[],
+ edges: GraphEdge[],
+ parentOf: (n: GraphNode) => string | null,
+ quality: Quality,
+) {
+ const [result, setResult] = useState<{ positions: Float32Array; radius: number; ids: string[] } | null>(
+ null,
+ );
+ const workerRef = useRef(null);
+ const requestId = useRef(0);
+ useEffect(() => {
+ const worker = new Worker(new URL('../workers/layout3d.worker.ts', import.meta.url), { type: 'module' });
+ workerRef.current = worker;
+ return () => worker.terminate();
+ }, []);
+ useEffect(() => {
+ const worker = workerRef.current;
+ if (!worker) return;
+ const id = ++requestId.current;
+ const ids = nodes.map((n) => n.id);
+ const layoutNodes: LayoutNode[] = nodes.map((n) => ({
+ id: n.id,
+ role: roleOf(n),
+ subsystem: (n.metadata.subsystem as string | undefined) ?? null,
+ parent: parentOf(n),
+ }));
+ const layoutEdges = edges
+ .filter((e) => e.type === 'IMPORTS' || e.type === 'CALLS' || e.type === 'USES' || e.type === 'EXTENDS')
+ .map((e) => ({ source: e.source, target: e.target }));
+ worker.onmessage = (e: MessageEvent) => {
+ if (e.data.requestId === requestId.current)
+ setResult({ positions: e.data.positions, radius: e.data.radius, ids });
+ };
+ worker.postMessage({
+ requestId: id,
+ nodes: layoutNodes,
+ edges: layoutEdges,
+ quality: quality === 'low' ? 0.2 : quality === 'high' ? 1 : 0.6,
+ } satisfies Layout3DRequest);
+ }, [nodes, edges, parentOf, quality]);
+ return result;
+}
+
+const FAMILIES: NodeFamily[] = [
+ 'structure',
+ 'file',
+ 'type',
+ 'callable',
+ 'data',
+ 'endpoint',
+ 'concept',
+ 'external',
+ 'value',
+];
+
+function FamilyGeometry({ family, quality }: { family: NodeFamily; quality: Quality }) {
+ const d = quality === 'low' ? 0 : 1;
+ switch (family) {
+ case 'structure':
+ return ;
+ case 'file':
+ return ;
+ case 'type':
+ return ;
+ case 'data':
+ return ;
+ case 'endpoint':
+ return ;
+ case 'concept':
+ return ;
+ case 'external':
+ return ;
+ default:
+ return ;
+ }
+}
+
+interface SceneProps {
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+ positions: Float32Array;
+ radius: number;
+ palette: Palette;
+ quality: Quality;
+ labelBudget: number;
+ reducedMotion: boolean;
+ focusBehavior: 'neighbourhood' | 'node' | 'none';
+ apiRef: MutableRefObject;
+ onExpand: (id: string) => void;
+ onLabels: (items: LabelItem[]) => void;
+ labelEls: MutableRefObject>;
+}
+
+interface LabelItem {
+ id: string;
+ text: string;
+ glyph: string;
+ major: boolean;
+ root: boolean;
+ on: boolean;
+ dim: boolean;
+ detail: string | null;
+}
+
+function AtlasScene({
+ nodes,
+ edges,
+ positions,
+ radius,
+ palette,
+ quality,
+ labelBudget,
+ reducedMotion,
+ focusBehavior,
+ apiRef,
+ onExpand,
+ onLabels,
+ labelEls,
+}: SceneProps) {
+ const { camera, invalidate, gl, scene } = useThree();
+ const controls = useThree((s) => s.controls) as OrbitControlsImpl | null;
+ const selectedIds = useWorkspace((s) => s.selectedIds);
+ const primaryId = useWorkspace((s) => s.primaryId);
+ const hoverId = useWorkspace((s) => s.hoverId);
+ const highlight = useWorkspace((s) => s.highlight);
+ const revealTick = useWorkspace((s) => s.revealTick);
+ const [far, setFar] = useState(false);
+ const [nearLabels, setNearLabels] = useState([]);
+
+ const indexOf = useMemo(() => new Map(nodes.map((n, i) => [n.id, i])), [nodes]);
+ const pos = useCallback(
+ (i: number) => new THREE.Vector3(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]),
+ [positions],
+ );
+ const byFamily = useMemo(() => {
+ const m = new Map();
+ nodes.forEach((n, i) => {
+ const f = NODE_FAMILY[n.type];
+ (m.get(f) ?? m.set(f, []).get(f)!).push(i);
+ });
+ return m;
+ }, [nodes]);
+
+ // ---- which nodes and edges are emphasised ------------------------------
+ const active = useMemo(() => {
+ const centre = hoverId ?? primaryId;
+ if (centre && indexOf.has(centre)) {
+ const nb = neighbourhood(centre, edges);
+ for (const id of selectedIds) nb.nodes.add(id);
+ return nb;
+ }
+ if (highlight) return { nodes: new Set(highlight.nodeIds), edges: new Set(highlight.edgeIds) };
+ return null;
+ }, [hoverId, primaryId, selectedIds, highlight, edges, indexOf]);
+
+ // ---- instanced nodes ----------------------------------------------------
+ const meshes = useRef(new Map());
+ useLayoutEffect(() => {
+ const m4 = new THREE.Matrix4();
+ const q = new THREE.Quaternion();
+ const color = new THREE.Color();
+ const selected = new Set(selectedIds);
+ for (const [family, list] of byFamily) {
+ const mesh = meshes.current.get(family);
+ if (!mesh) continue;
+ list.forEach((ni, k) => {
+ const n = nodes[ni]!;
+ const emphasised = selected.has(n.id) || n.id === hoverId;
+ const s = NODE_SIZE[n.type] * 0.42 * (emphasised ? 1.5 : 1);
+ m4.compose(pos(ni), q, new THREE.Vector3(s, s, s));
+ mesh.setMatrixAt(k, m4);
+ const base = emphasised ? palette.selection : nodeColor(n.type, palette);
+ color.set(active && !active.nodes.has(n.id) && !emphasised ? mix(base, palette.canvas, 0.72) : base);
+ mesh.setColorAt(k, color);
+ });
+ mesh.count = list.length;
+ mesh.instanceMatrix.needsUpdate = true;
+ if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
+ mesh.computeBoundingSphere();
+ }
+ invalidate();
+ }, [byFamily, nodes, pos, palette, selectedIds, hoverId, active, invalidate]);
+
+ // ---- edges: two buffered line sets (verified solid, derived dashed) ------
+ const edgeSets = useMemo(() => {
+ const build = (list: GraphEdge[]) => {
+ // Cross-file relationships first, so symbol-level detail can be cut with a draw range when far away.
+ const coarse: GraphEdge[] = [];
+ const fine: GraphEdge[] = [];
+ for (const e of list) {
+ const a = nodes[indexOf.get(e.source)!]!;
+ const b = nodes[indexOf.get(e.target)!]!;
+ if (
+ (isSymbolType(a.type) && isSymbolType(b.type) && a.filePath === b.filePath) ||
+ e.type === 'CONTAINS'
+ )
+ fine.push(e);
+ else coarse.push(e);
+ }
+ const ordered = [...coarse, ...fine];
+ const geometry = new THREE.BufferGeometry();
+ const p = new Float32Array(ordered.length * 6);
+ ordered.forEach((e, k) => {
+ const a = indexOf.get(e.source)! * 3;
+ const b = indexOf.get(e.target)! * 3;
+ p.set(
+ [
+ positions[a]!,
+ positions[a + 1]!,
+ positions[a + 2]!,
+ positions[b]!,
+ positions[b + 1]!,
+ positions[b + 2]!,
+ ],
+ k * 6,
+ );
+ });
+ geometry.setAttribute('position', new THREE.BufferAttribute(p, 3));
+ geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(ordered.length * 6), 3));
+ return { geometry, ordered, coarseCount: coarse.length };
+ };
+ const usable = edges.filter((e) => indexOf.has(e.source) && indexOf.has(e.target));
+ return {
+ verified: build(usable.filter((e) => e.verified)),
+ derived: build(usable.filter((e) => !e.verified)),
+ };
+ }, [edges, nodes, indexOf, positions]);
+ useEffect(
+ () => () => {
+ edgeSets.verified.geometry.dispose();
+ edgeSets.derived.geometry.dispose();
+ },
+ [edgeSets],
+ );
+ const derivedLine = useRef(null);
+ useLayoutEffect(() => {
+ const c = new THREE.Color();
+ for (const [set, baseHex] of [
+ [edgeSets.verified, palette.textMuted],
+ [edgeSets.derived, palette.relationUnverified],
+ ] as const) {
+ const attr = set.geometry.getAttribute('color') as THREE.BufferAttribute;
+ set.ordered.forEach((e, k) => {
+ const on = active?.edges.has(e.id);
+ c.set(
+ on
+ ? e.verified
+ ? palette.relationActive
+ : palette.relationUnverified
+ : mix(baseHex, palette.canvas, active ? 0.88 : e.type === 'CONTAINS' || !e.verified ? 0.72 : 0.5),
+ );
+ attr.setXYZ(k * 2, c.r, c.g, c.b);
+ attr.setXYZ(k * 2 + 1, c.r, c.g, c.b);
+ });
+ attr.needsUpdate = true;
+ // Level of detail: from far away only cross-file relationships are drawn.
+ set.geometry.setDrawRange(0, (far && !active ? set.coarseCount : set.ordered.length) * 2);
+ }
+ derivedLine.current?.computeLineDistances();
+ invalidate();
+ }, [edgeSets, palette, active, far, invalidate]);
+
+ // ---- camera transitions -------------------------------------------------
+ const anim = useRef<{
+ fromP: THREE.Vector3;
+ fromT: THREE.Vector3;
+ toP: THREE.Vector3;
+ toT: THREE.Vector3;
+ start: number;
+ duration: number;
+ } | null>(null);
+ const flyTo = useCallback(
+ (toP: THREE.Vector3, toT: THREE.Vector3) => {
+ const fromT = controls ? controls.target.clone() : new THREE.Vector3();
+ anim.current = {
+ fromP: camera.position.clone(),
+ fromT,
+ toP,
+ toT,
+ start: performance.now(),
+ duration: reducedMotion ? 0 : 820,
+ };
+ invalidate();
+ },
+ [camera, controls, invalidate, reducedMotion],
+ );
+ useFrame(() => {
+ const a = anim.current;
+ if (!a) return;
+ const t = a.duration === 0 ? 1 : Math.min(1, (performance.now() - a.start) / a.duration);
+ const k = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
+ camera.position.lerpVectors(a.fromP, a.toP, k);
+ if (controls) {
+ controls.target.lerpVectors(a.fromT, a.toT, k);
+ controls.update();
+ } else camera.lookAt(a.toT);
+ if (t >= 1) anim.current = null;
+ else invalidate();
+ });
+ const home = useCallback(
+ () => flyTo(new THREE.Vector3(0, radius * 1.25, radius * 3.3), new THREE.Vector3(0, 0, 0)),
+ [flyTo, radius],
+ );
+ const focusOn = useCallback(
+ (id: string) => {
+ const i = indexOf.get(id);
+ if (i === undefined) return;
+ const n = nodes[i]!;
+ const target = pos(i);
+ const dist =
+ n.type === 'Repository'
+ ? radius * 2.6
+ : n.type === 'Subsystem'
+ ? radius * 1.3
+ : n.type === 'Concept' || n.type === 'Package'
+ ? radius * 0.7
+ : isSymbolType(n.type)
+ ? 34
+ : 46;
+ // Look at the node from outside the structure, keeping the current side when the node is central.
+ const dir =
+ target.lengthSq() > 4 ? target.clone().normalize() : camera.position.clone().sub(target).normalize();
+ // Step to the side and up, so the node is not seen in line with its parent and the centre.
+ const side = new THREE.Vector3(0, 1, 0).cross(dir);
+ if (side.lengthSq() > 0.01) dir.addScaledVector(side.normalize(), 0.75);
+ dir.y += 0.45;
+ flyTo(target.clone().add(dir.normalize().multiplyScalar(dist)), target);
+ },
+ [indexOf, nodes, pos, radius, camera, flyTo],
+ );
+ useEffect(() => {
+ apiRef.current = {
+ reset: home,
+ focusOn,
+ capture: () => {
+ gl.render(scene, camera);
+ return gl.domElement.toDataURL('image/png');
+ },
+ };
+ return () => {
+ apiRef.current = null;
+ };
+ }, [apiRef, home, focusOn, gl, scene, camera]);
+ // Frame the whole structure whenever a new layout arrives.
+ const framed = useRef(null);
+ useEffect(() => {
+ // Runs again once the controls exist, so the first frame is drawn from the home position.
+ const key = `${radius}:${controls ? 1 : 0}`;
+ if (framed.current === key) return;
+ framed.current = key;
+ camera.position.set(0, radius * 1.25, radius * 3.3);
+ controls?.target.set(0, 0, 0);
+ controls?.update();
+ invalidate();
+ }, [radius, camera, controls, invalidate]);
+ // With on-demand rendering nothing is drawn until a frame is requested: ask for one whenever the
+ // scene content is (re)created, and once more after the browser has laid the canvas out.
+ useEffect(() => {
+ invalidate();
+ const t = setTimeout(() => invalidate(), 150);
+ return () => clearTimeout(t);
+ }, [controls, positions, invalidate]);
+ useEffect(() => {
+ if (revealTick === 0 || focusBehavior === 'none') return;
+ const s = useWorkspace.getState();
+ const id = s.primaryId ?? s.focusId;
+ if (id) focusOn(id);
+ else home();
+ }, [revealTick]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // ---- level of detail and label culling, recomputed when the camera rests --
+ const updateDetail = useCallback(() => {
+ const d = camera.position.length();
+ setFar(d > radius * 2.1 && edges.length > 600);
+ if (labelBudget === 0) return setNearLabels([]);
+ const scored: [number, string][] = [];
+ const v = new THREE.Vector3();
+ const frustum = new THREE.Frustum().setFromProjectionMatrix(
+ new THREE.Matrix4().multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse),
+ );
+ for (let i = 0; i < nodes.length; i++) {
+ const n = nodes[i]!;
+ if (n.type === 'Repository' || n.type === 'Subsystem' || n.type === 'Concept') continue;
+ v.set(positions[i * 3]!, positions[i * 3 + 1]!, positions[i * 3 + 2]!);
+ if (!frustum.containsPoint(v)) continue;
+ // Larger nodes earn a label from further away.
+ scored.push([v.distanceTo(camera.position) / (NODE_SIZE[n.type] + 1), n.id]);
+ }
+ scored.sort((a, b) => a[0] - b[0]);
+ const limit = (isSymbol: boolean) => (isSymbol ? 18 : 60);
+ setNearLabels(
+ scored
+ .filter(
+ ([s, id]) =>
+ s * (NODE_SIZE[nodes[indexOf.get(id)!]!.type] + 1) <
+ limit(isSymbolType(nodes[indexOf.get(id)!]!.type)) * 1.4,
+ )
+ .slice(0, labelBudget)
+ .map(([, id]) => id),
+ );
+ }, [camera, radius, edges.length, labelBudget, nodes, positions, indexOf]);
+ useEffect(() => {
+ const t = setTimeout(updateDetail, 80);
+ return () => clearTimeout(t);
+ }, [updateDetail]);
+
+ // ---- pointer interaction ------------------------------------------------
+ const idAt = (family: NodeFamily, instanceId: number | undefined) =>
+ instanceId === undefined ? null : (nodes[byFamily.get(family)?.[instanceId] ?? -1]?.id ?? null);
+ const handlers = (family: NodeFamily) => ({
+ onClick: (e: ThreeEvent) => {
+ e.stopPropagation();
+ const id = idAt(family, e.instanceId);
+ if (!id) return;
+ const additive = e.nativeEvent.ctrlKey || e.nativeEvent.metaKey || e.nativeEvent.shiftKey;
+ useWorkspace.getState().select(id, { additive });
+ if (!additive && focusBehavior !== 'none') focusOn(id);
+ },
+ onDoubleClick: (e: ThreeEvent) => {
+ e.stopPropagation();
+ const id = idAt(family, e.instanceId);
+ if (id) onExpand(id);
+ },
+ ...(nodes.length <= 20000
+ ? {
+ onPointerMove: (e: ThreeEvent) => {
+ e.stopPropagation();
+ useWorkspace.getState().setHover(idAt(family, e.instanceId));
+ gl.domElement.style.cursor = 'pointer';
+ },
+ onPointerOut: () => {
+ useWorkspace.getState().setHover(null);
+ gl.domElement.style.cursor = '';
+ },
+ }
+ : {}),
+ });
+
+ // ---- labels -------------------------------------------------------------
+ const labels = useMemo(() => {
+ const ids = new Set();
+ for (const n of nodes)
+ if (n.type === 'Repository' || n.type === 'Subsystem' || n.type === 'Concept') ids.add(n.id);
+ for (const id of selectedIds) ids.add(id);
+ if (hoverId) ids.add(hoverId);
+ if (active) [...active.nodes].slice(0, 28).forEach((id) => ids.add(id));
+ for (const id of nearLabels) ids.add(id);
+ return [...ids].filter((id) => indexOf.has(id));
+ }, [nodes, selectedIds, hoverId, active, nearLabels, indexOf]);
+
+ // Labels are plain DOM elements in an overlay owned by the parent. The parent renders them;
+ // this scene only moves them, once per rendered frame, by projecting their node to the screen.
+ const labelItems = useMemo(
+ () =>
+ labels.map((id) => {
+ const n = nodes[indexOf.get(id)!]!;
+ const major = n.type === 'Repository' || n.type === 'Subsystem' || n.type === 'Concept';
+ const on = id === hoverId || selectedIds.includes(id);
+ return {
+ id,
+ text: n.name,
+ glyph: FAMILY_GLYPH[NODE_FAMILY[n.type]],
+ major,
+ root: n.type === 'Repository',
+ on,
+ dim: !!active && !active.nodes.has(id) && !on,
+ detail: id === hoverId && !major ? NODE_TYPE_LABELS[n.type] : null,
+ };
+ }),
+ [labels, nodes, indexOf, hoverId, selectedIds, active],
+ );
+ useEffect(() => {
+ onLabels(labelItems);
+ invalidate();
+ const t = setTimeout(() => invalidate(), 40);
+ return () => clearTimeout(t);
+ }, [labelItems, onLabels, invalidate]);
+ const projected = useMemo(() => new THREE.Vector3(), []);
+ useFrame(({ size }) => {
+ for (const item of labelItems) {
+ const el = labelEls.current.get(item.id);
+ const i = indexOf.get(item.id);
+ if (!el || i === undefined) continue;
+ projected
+ .set(
+ positions[i * 3]!,
+ positions[i * 3 + 1]! + NODE_SIZE[nodes[i]!.type] * 0.42 + 0.6,
+ positions[i * 3 + 2]!,
+ )
+ .project(camera);
+ // Behind the camera or outside the view: hide instead of drawing at a mirrored position.
+ if (projected.z > 1 || Math.abs(projected.x) > 1.1 || Math.abs(projected.y) > 1.1) {
+ el.style.visibility = 'hidden';
+ continue;
+ }
+ el.style.visibility = 'visible';
+ el.style.transform = `translate(${((projected.x + 1) / 2) * size.width}px, ${((1 - projected.y) / 2) * size.height}px) translate(-50%, -120%)`;
+ }
+ });
+
+ return (
+ <>
+
+ {FAMILIES.map((family) => {
+ const count = byFamily.get(family)?.length ?? 0;
+ if (!count) return null;
+ return (
+ {
+ if (m) meshes.current.set(family, m);
+ else meshes.current.delete(family);
+ }}
+ args={[undefined, undefined, count]}
+ frustumCulled={false}
+ {...handlers(family)}
+ >
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ >
+ );
+}
+
+export function Atlas() {
+ const { view, adj, palette, settings, reducedMotion, expandFile, openView, theme } = useWs();
+ const primaryId = useWorkspace((s) => s.primaryId);
+ const focusId = useWorkspace((s) => s.focusId);
+ const [gl, setGl] = useState<'ok' | 'unsupported' | 'lost'>(() =>
+ webglAvailable() ? 'ok' : 'unsupported',
+ );
+ const [attempt, setAttempt] = useState(0);
+ const apiRef = useRef(null);
+ const [labelItems, setLabelItems] = useState([]);
+ const labelEls = useRef(new Map());
+ const quality = settings.performance.renderingQuality;
+
+ const parentOf = useCallback(
+ (n: GraphNode): string | null => {
+ if (!isSymbolType(n.type)) return null;
+ const parentEdge = (adj.in.get(n.id) ?? []).find((e) => e.type === 'CONTAINS');
+ return parentEdge?.source ?? fileIdOf(n);
+ },
+ [adj],
+ );
+ const layout = useLayout3d(view.nodes, view.edges, parentOf, quality);
+ const current =
+ layout &&
+ layout.ids.length === view.nodes.length &&
+ layout.ids[0] === view.nodes[0]?.id &&
+ layout.ids[layout.ids.length - 1] === view.nodes[view.nodes.length - 1]?.id
+ ? layout
+ : null;
+
+ // The scene keeps showing the previous arrangement until the next one is ready, so changing a
+ // filter or entering focus mode never blanks the view or tears down the renderer.
+ const [shown, setShown] = useState<{
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+ positions: Float32Array;
+ radius: number;
+ } | null>(null);
+ useEffect(() => {
+ if (current)
+ setShown({
+ nodes: view.nodes,
+ edges: view.edges,
+ positions: current.positions,
+ radius: current.radius,
+ });
+ }, [current]); // eslint-disable-line react-hooks/exhaustive-deps
+ const alive = useRef(true);
+ useEffect(() => {
+ alive.current = true;
+ return () => {
+ alive.current = false;
+ };
+ }, []);
+
+ const onExpand = useCallback(
+ (id: string) => {
+ const n = adj.nodes.get(id);
+ if (!n) return;
+ if (n.type === 'File' || n.type === 'Configuration') return expandFile(id);
+ if (n.type === 'Subsystem' || n.type === 'Concept') {
+ // Unfold the files of a region.
+ (adj.in.get(id) ?? [])
+ .filter((e) => e.type === 'BELONGS_TO')
+ .slice(0, 60)
+ .forEach((e) => expandFile(e.source, true));
+ return;
+ }
+ const fid = fileIdOf(n);
+ if (fid) expandFile(fid, true);
+ },
+ [adj, expandFile],
+ );
+
+ const primary = primaryId ? adj.nodes.get(primaryId) : undefined;
+ const crumbs = useMemo(() => {
+ const out: GraphNode[] = [];
+ const repo = adj.nodes.get('repo:root');
+ if (repo) out.push(repo);
+ if (!primary || primary.type === 'Repository') return out;
+ const sub = adj.nodes.get((primary.metadata.subsystem as string | undefined) ?? '');
+ if (sub && sub.id !== primary.id) out.push(sub);
+ const file = isSymbolType(primary.type) ? adj.nodes.get(fileIdOf(primary) ?? '') : undefined;
+ if (file) out.push(file);
+ const parent = isSymbolType(primary.type)
+ ? adj.nodes.get((adj.in.get(primary.id) ?? []).find((e) => e.type === 'CONTAINS')?.source ?? '')
+ : undefined;
+ if (parent && isSymbolType(parent.type)) out.push(parent);
+ out.push(primary);
+ return out;
+ }, [primary, adj]);
+ const subsystemId =
+ (primary?.metadata.subsystem as string | undefined) ??
+ (primary?.type === 'Subsystem' ? primary.id : undefined);
+ const labelBudget = quality === 'low' ? 0 : { low: 6, medium: 16, high: 40 }[settings.graph.labelDensity];
+
+ if (gl !== 'ok')
+ return (
+
+
+ {gl === 'lost' ? 'The 3D view lost its graphics context' : '3D rendering is not available here'}
+
+
+ {gl === 'lost'
+ ? 'The browser reclaimed the graphics memory used by the Atlas. Your selection and filters are kept.'
+ : 'This browser or device does not provide WebGL. The same graph is available in two dimensions.'}
+
+
+ openView('graph')}>
+ Open the 2D Graph Explorer
+
+ {gl === 'lost' ? (
+ {
+ setAttempt((a) => a + 1);
+ setGl('ok');
+ }}
+ >
+ Start the 3D view again
+
+ ) : null}
+
+
+ );
+
+ return (
+
+
+ apiRef.current?.reset()}
+ data-testid="atlas-reset"
+ >
+
+ Reset camera
+
+ {subsystemId ? (
+ apiRef.current?.focusOn(subsystemId)}>
+ Focus subsystem
+
+ ) : null}
+ {primary && focusId !== primary.id ? (
+ useWorkspace.getState().setFocus(primary.id)}
+ >
+ Show connected neighbourhood
+
+ ) : null}
+ {
+ useWorkspace.getState().setFocus(null);
+ useWorkspace.getState().clearSelection();
+ apiRef.current?.reset();
+ }}
+ >
+
+ Global view
+
+ {
+ const url = apiRef.current?.capture();
+ if (url) downloadUrl('vectora-atlas.png', url);
+ }}
+ >
+
+ PNG
+
+
+ {shown ? (
+
{
+ const canvas = renderer.domElement;
+ canvas.addEventListener('webglcontextlost', (e) => {
+ e.preventDefault();
+ // Disposing the renderer on the way out also reports a lost context; only a loss
+ // while the view is on screen is a real one.
+ if (alive.current && canvas.isConnected) setGl('lost');
+ });
+ }}
+ onPointerMissed={(e) => {
+ if (e.type === 'click') useWorkspace.getState().clearSelection();
+ }}
+ aria-label="3D knowledge atlas. Drag to orbit, right-drag to pan, scroll to zoom. Use Search to move to a node."
+ role="application"
+ >
+
+
+
+
+
+
+ ) : (
+
+ )}
+ {shown ? (
+
+ {labelItems.map((l) => (
+ {
+ if (el) labelEls.current.set(l.id, el);
+ else labelEls.current.delete(l.id);
+ }}
+ className={l.major ? 'font-editorial' : 'font-mono'}
+ style={{
+ position: 'absolute',
+ left: 0,
+ top: 0,
+ visibility: 'hidden',
+ whiteSpace: 'nowrap',
+ fontSize: l.major ? (l.root ? 15 : 13) : 10.5,
+ color: l.on || l.major ? palette.text : palette.textSecondary,
+ opacity: l.dim ? 0.25 : 1,
+ background: l.on ? palette.elevated : 'transparent',
+ border: l.on ? `1px solid ${palette.line}` : 'none',
+ padding: l.on ? '1px 6px' : 0,
+ zIndex: l.on ? 2 : 1,
+ }}
+ >
+ {l.glyph} {l.text}
+ {l.detail ? — {l.detail} : null}
+
+ ))}
+
+ ) : null}
+
+ {crumbs.map((c, i) => (
+
+ {i > 0 ? : null}
+ {
+ if (c.type === 'Repository') {
+ useWorkspace.getState().clearSelection();
+ apiRef.current?.reset();
+ } else {
+ useWorkspace.getState().select(c.id);
+ apiRef.current?.focusOn(c.id);
+ }
+ }}
+ >
+ {c.name}
+
+
+ ))}
+
+ {primary ? null : (
+
+ Drag to orbit, right-drag to pan, scroll to zoom, double-click to unfold
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/workspace/Dependencies.tsx b/apps/web/src/workspace/Dependencies.tsx
new file mode 100644
index 0000000..ddb9119
--- /dev/null
+++ b/apps/web/src/workspace/Dependencies.tsx
@@ -0,0 +1,401 @@
+import { useMemo, useState } from 'react';
+import { EDGE_TYPE_LABELS } from '@vectora/graph-schema';
+import { Empty, SectionHeading, cx } from '@/components/ui';
+import { useAnalysis } from '@/lib/api';
+import { useWorkspace } from '@/lib/store';
+import { useWs } from './context';
+
+type Tab = 'packages' | 'subsystems' | 'cycles' | 'flows' | 'unresolved';
+
+/** Finds groups of files that import each other (strongly connected components, Tarjan). */
+function importCycles(nodes: string[], out: Map): string[][] {
+ const index = new Map();
+ const low = new Map();
+ const stack: string[] = [];
+ const on = new Set();
+ const result: string[][] = [];
+ let counter = 0;
+ for (const root of nodes) {
+ if (index.has(root)) continue;
+ const work: { id: string; i: number }[] = [{ id: root, i: 0 }];
+ index.set(root, counter);
+ low.set(root, counter++);
+ stack.push(root);
+ on.add(root);
+ while (work.length) {
+ const f = work[work.length - 1]!;
+ const next = out.get(f.id) ?? [];
+ if (f.i < next.length) {
+ const w = next[f.i++]!;
+ if (!index.has(w)) {
+ index.set(w, counter);
+ low.set(w, counter++);
+ stack.push(w);
+ on.add(w);
+ work.push({ id: w, i: 0 });
+ } else if (on.has(w)) low.set(f.id, Math.min(low.get(f.id)!, index.get(w)!));
+ } else {
+ work.pop();
+ const parent = work[work.length - 1];
+ if (parent) low.set(parent.id, Math.min(low.get(parent.id)!, low.get(f.id)!));
+ if (low.get(f.id) === index.get(f.id)) {
+ const comp: string[] = [];
+ let w: string;
+ do {
+ w = stack.pop()!;
+ on.delete(w);
+ comp.push(w);
+ } while (w !== f.id);
+ if (comp.length > 1) result.push(comp);
+ }
+ }
+ }
+ }
+ return result.sort((a, b) => b.length - a.length);
+}
+
+export function Dependencies() {
+ const { repo, graph, adj, reveal, openView, settings } = useWs();
+ const { data: analysis } = useAnalysis(repo.id);
+ const setHighlight = useWorkspace((s) => s.setHighlight);
+ const [tab, setTab] = useState('packages');
+ const [filter, setFilter] = useState('');
+
+ const packages = useMemo(
+ () =>
+ graph.nodes
+ .filter((n) => n.type === 'Package')
+ .map((p) => {
+ const importers = (adj.in.get(p.id) ?? []).filter((e) => e.type === 'IMPORTS');
+ const declaredBy = (adj.in.get(p.id) ?? []).filter((e) => e.type === 'DEPENDS_ON');
+ return { node: p, importers, declaredBy, files: new Set(importers.map((e) => e.source)).size };
+ })
+ .sort((a, b) => b.files - a.files || (a.node.name < b.node.name ? -1 : 1)),
+ [graph, adj],
+ );
+ const subsystemDeps = useMemo(
+ () =>
+ graph.edges
+ .filter((e) => e.type === 'DEPENDS_ON' && e.source.startsWith('sub:'))
+ .sort((a, b) => Number(b.metadata?.count ?? 0) - Number(a.metadata?.count ?? 0)),
+ [graph],
+ );
+ const cycles = useMemo(() => {
+ const out = new Map();
+ const files = new Set();
+ for (const e of graph.edges) {
+ if (e.type !== 'IMPORTS' || !e.target.startsWith('file:') || e.source === e.target) continue;
+ (out.get(e.source) ?? out.set(e.source, []).get(e.source)!).push(e.target);
+ files.add(e.source);
+ files.add(e.target);
+ }
+ return importCycles([...files], out);
+ }, [graph]);
+
+ const showIn = (label: string, nodeIds: string[], edgeIds: string[] = []) => {
+ setHighlight({ label, nodeIds, edgeIds });
+ useWorkspace.getState().clearSelection();
+ openView(settings.graph.defaultMode === 'mindmap' ? 'graph' : settings.graph.defaultMode);
+ };
+ const q = filter.trim().toLowerCase();
+ const tabs: [Tab, string, number][] = [
+ ['packages', 'External packages', packages.length],
+ ['subsystems', 'Between subsystems', subsystemDeps.length],
+ ['cycles', 'Circular imports', cycles.length],
+ ['flows', 'Execution flows', analysis?.flows.length ?? 0],
+ ['unresolved', 'Unresolved references', analysis?.unresolvedCount ?? 0],
+ ];
+
+ return (
+
+
+
Dependency Explorer
+
+ {tabs.map(([id, label, count]) => (
+ setTab(id)}
+ >
+ {label} {count.toLocaleString()}
+
+ ))}
+
+
+
+ {tab === 'packages' ? (
+ packages.length === 0 ? (
+
+ No manifest declares a dependency and no source file imports one.
+
+ ) : (
+
+
setFilter(e.target.value)}
+ />
+
+
+
+ Package
+ Version
+ Declared in
+ Importing files
+
+
+
+
+ {packages
+ .filter((p) => !q || p.node.name.toLowerCase().includes(q))
+ .slice(0, 400)
+ .map((p) => (
+
+
+ reveal(p.node.id)}
+ >
+ {p.node.name}
+
+ {p.node.metadata.dev ? dev : null}
+
+ {(p.node.metadata.version as string | null) ?? ''}
+
+ {p.declaredBy.length ? (
+ p.declaredBy.map((e) => `${e.location?.path}:${e.location?.startLine}`).join(', ')
+ ) : (
+ not declared in a manifest
+ )}
+
+ {p.files || not imported }
+
+ {p.files ? (
+
+ showIn(
+ `imports of ${p.node.name}`,
+ [p.node.id, ...p.importers.map((e) => e.source)],
+ p.importers.map((e) => e.id),
+ )
+ }
+ >
+ Show in graph
+
+ ) : null}
+
+
+ ))}
+
+
+
+ )
+ ) : null}
+
+ {tab === 'subsystems' ? (
+ subsystemDeps.length === 0 ? (
+
+ Either the repository has a single subsystem or no relationship crosses a subsystem boundary.
+
+ ) : (
+
+ {subsystemDeps.map((e) => (
+
+
+
+ reveal(e.source)}
+ >
+ {adj.nodes.get(e.source)?.name}
+ {' '}
+ depends on {' '}
+ reveal(e.target)}
+ >
+ {adj.nodes.get(e.target)?.name}
+
+
+
+ {Object.entries((e.metadata?.kinds as Record) ?? {})
+ .map(([k, v]) => `${v} ${EDGE_TYPE_LABELS[k as keyof typeof EDGE_TYPE_LABELS]}`)
+ .join(', ')}
+
+
+ {e.evidence}
+
+ ))}
+
+ )
+ ) : null}
+
+ {tab === 'cycles' ? (
+ cycles.length === 0 ? (
+
+ No group of files imports itself through resolved import relationships.
+
+ ) : (
+
+ {cycles.map((c, i) => (
+
+
+
{c.length} files import each other
+
+ showIn(
+ `import cycle of ${c.length} files`,
+ c,
+ graph.edges
+ .filter(
+ (e) => e.type === 'IMPORTS' && c.includes(e.source) && c.includes(e.target),
+ )
+ .map((e) => e.id),
+ )
+ }
+ >
+ Show in graph
+
+
+
+ {c.slice(0, 12).map((id) => (
+
+ reveal(id)}
+ >
+ {adj.nodes.get(id)?.filePath}
+
+
+ ))}
+ {c.length > 12 ? {c.length - 12} more : null}
+
+
+ ))}
+
+ )
+ ) : null}
+
+ {tab === 'flows' ? (
+ !analysis?.flows.length ? (
+
+ No API endpoint or entry point with resolved outgoing calls was found.
+
+ ) : (
+
+
+ Flows follow route and call relationships from an endpoint or entry point. They are derived
+ from static structure; callbacks and runtime wiring are not included.
+
+
+ {analysis.flows.map((f) => (
+
+
+
+ {f.name}{' '}
+ {f.reachesData ? reaches data access : null}
+
+
showIn(f.name, f.steps)}>
+ Show in graph
+
+
+
+ {f.steps.map((s, i) => (
+
+ {i > 0 ? (
+
+ ›
+
+ ) : null}
+ reveal(s)}
+ >
+ {adj.nodes.get(s)?.name ?? s}
+
+
+ ))}
+
+
+ ))}
+
+
+ )
+ ) : null}
+
+ {tab === 'unresolved' ? (
+ !analysis?.unresolvedCount ? (
+
+ Every reference the analysers found was tied to a definition.
+
+ ) : (
+
+
+ These references were found in the source but could not be tied to a definition with scope
+ evidence. They are reported here and are not part of the graph.
+
+
By reason
+
+ {Object.entries(analysis.unresolvedByReason)
+ .sort((a, b) => b[1] - a[1])
+ .map(([reason, count]) => (
+
+ {reason}
+ {count.toLocaleString()}
+
+ ))}
+
+
Samples
+
+
+
+ Reference
+ Kind
+ Location
+ Reason
+
+
+
+ {analysis.unresolvedSamples.slice(0, 300).map((u, i) => (
+
+ {u.name}
+
+ {EDGE_TYPE_LABELS[u.kind as keyof typeof EDGE_TYPE_LABELS] ?? u.kind}
+
+
+
+ openView('source', `path=${encodeURIComponent(u.fromPath)}&line=${u.line}`)
+ }
+ >
+ {u.fromPath}:{u.line}
+
+
+ {u.reason}
+
+ ))}
+
+
+
+ )
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/src/workspace/Documentation.tsx b/apps/web/src/workspace/Documentation.tsx
new file mode 100644
index 0000000..44706ef
--- /dev/null
+++ b/apps/web/src/workspace/Documentation.tsx
@@ -0,0 +1,171 @@
+import { useQuery } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import Markdown from 'react-markdown';
+import { useSearchParams } from 'react-router';
+import { CodePreview } from '@/components/CodePreview';
+import { Empty, Loading, cx } from '@/components/ui';
+import { api, useSource } from '@/lib/api';
+import { useWs } from './context';
+
+/** Documentation files found in the repository, plus documents generated from the graph. */
+export function Documentation() {
+ const { repo } = useWs();
+ const [params, setParams] = useSearchParams();
+ const docs = useQuery({ queryKey: ['docs', repo.id], queryFn: () => api.docs(repo.id) });
+ const generated = params.get('generated');
+ const path = params.get('path');
+ useEffect(() => {
+ if (!path && !generated && docs.data)
+ setParams(docs.data[0] ? { path: docs.data[0].path } : { generated: 'architecture' }, {
+ replace: true,
+ });
+ }, [path, generated, docs.data, setParams]);
+ const source = useSource(repo.id, path);
+ const gen = useQuery({
+ queryKey: ['generated-doc', repo.id, generated],
+ queryFn: async () => (await fetch(api.exportUrl(repo.id, generated!, false))).text(),
+ enabled: !!generated,
+ });
+ const content = generated ? gen.data : source.data?.content;
+ const isMarkdown = !!generated || /\.(md|mdx|markdown)$/i.test(path ?? '');
+
+ return (
+
+
+ Documentation
+ Generated from the graph
+ {(
+ [
+ ['architecture', 'Architecture'],
+ ['markdown', 'Repository analysis'],
+ ] as const
+ ).map(([id, label]) => (
+ setParams({ generated: id })}
+ >
+ {label}
+
+ ))}
+ In the repository
+ {docs.isLoading ? Loading…
: null}
+ {docs.data?.length === 0 ? No documentation files were found.
: null}
+ {docs.data?.map((d) => (
+ setParams({ path: d.path })}
+ >
+
+ {d.title}
+ {d.path}
+
+
+ ))}
+
+
+ {(generated ? gen.isLoading : source.isLoading) ? (
+
+ ) : (generated ? gen.error : source.error) ? (
+
+ {((generated ? gen.error : source.error) as Error).message}
+
+ ) : content === undefined ? null : isMarkdown ? (
+
+ {generated ? (
+
+ Generated from the knowledge graph. Download it from Export in the header.
+
+ ) : (
+ {path}
+ )}
+ {/* react-markdown renders to React elements and ignores raw HTML, so repository text cannot inject markup. */}
+ (/^(https?:|mailto:|#)/i.test(url) ? url : '')}
+ components={{
+ a: ({ href, children }) =>
+ href ? (
+
+ {children}
+
+ ) : (
+ {children}
+ ),
+ img: ({ alt }) => [image: {alt || 'no description'}] ,
+ }}
+ >
+ {content}
+
+
+ ) : (
+
+ )}
+ {!generated && source.data?.truncated ? (
+
The file is larger than the preview limit and was shortened.
+ ) : null}
+
+
+ );
+}
+
+/** Full-file source viewer, opened from “Open source location”. */
+export function SourceView() {
+ const { repo, adj, reveal, openView, settings } = useWs();
+ const [params] = useSearchParams();
+ const path = params.get('path');
+ const line = Number(params.get('line') ?? 1) || 1;
+ const { data, isLoading, error } = useSource(repo.id, path);
+ if (!path) return Open a source location from the inspector. ;
+ if (isLoading) return ;
+ if (error || !data)
+ return {(error as Error | null)?.message} ;
+ const fileId = `file:${data.path}`;
+ return (
+
+
+
+
{data.path}
+
+ {data.totalLines.toLocaleString()} lines{data.language ? `, ${data.language}` : ''}
+ {data.truncated ? ', shortened to the preview limit' : ''} — line {line}
+
+
+ {adj.nodes.has(fileId) ? (
+
{
+ reveal(fileId);
+ openView(settings.graph.defaultMode);
+ }}
+ >
+ Show this file in the graph
+
+ ) : null}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/workspace/GraphExplorer.tsx b/apps/web/src/workspace/GraphExplorer.tsx
new file mode 100644
index 0000000..384fb99
--- /dev/null
+++ b/apps/web/src/workspace/GraphExplorer.tsx
@@ -0,0 +1,603 @@
+import Graph from 'graphology';
+import forceAtlas2 from 'graphology-layout-forceatlas2';
+import FA2Layout from 'graphology-layout-forceatlas2/worker';
+import noverlap from 'graphology-layout-noverlap';
+import { Image as ImageIcon, Maximize, RotateCw } from 'lucide-react';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import Sigma from 'sigma';
+import { seed2d } from '@vectora/graph-layout';
+import { EDGE_TYPE_LABELS, type GraphNode, isSymbolType } from '@vectora/graph-schema';
+import { ViewToolbar } from '@/components/ViewControls';
+import { downloadUrl } from '@/lib/api';
+import { fileIdOf, neighbourhood } from '@/lib/graphView';
+import { useWorkspace } from '@/lib/store';
+import {
+ FAMILY_GLYPH,
+ NODE_FAMILY,
+ NODE_SIZE,
+ STRUCTURE_EDGES,
+ type Palette,
+ mix,
+ nodeColor,
+} from '@/lib/tokens';
+import { useWs } from './context';
+
+type Grouping = 'subsystem' | 'community';
+
+interface RenderState {
+ selected: Set;
+ primary: string | null;
+ hover: string | null;
+ activeNodes: Set | null;
+ activeEdges: Set | null;
+ palette: Palette;
+}
+
+const LABEL_THRESHOLD = { low: 11, medium: 7.5, high: 4.5 } as const;
+const LABEL_DENSITY = { low: 0.4, medium: 0.9, high: 1.8 } as const;
+
+export function GraphExplorer() {
+ const { view, adj, palette, settings, reducedMotion, expandFile, theme } = useWs();
+ const selectedIds = useWorkspace((s) => s.selectedIds);
+ const primaryId = useWorkspace((s) => s.primaryId);
+ const hoverId = useWorkspace((s) => s.hoverId);
+ const highlight = useWorkspace((s) => s.highlight);
+ const revealTick = useWorkspace((s) => s.revealTick);
+ const container = useRef(null);
+ const minimap = useRef(null);
+ const sigmaRef = useRef(null);
+ const graphRef = useRef(null);
+ const layoutRef = useRef<{ stop: () => void } | null>(null);
+ const stateRef = useRef({
+ selected: new Set(),
+ primary: null,
+ hover: null,
+ activeNodes: null,
+ activeEdges: null,
+ palette,
+ });
+ const viewRef = useRef(view);
+ viewRef.current = view;
+ const [grouping, setGrouping] = useState('subsystem');
+ const [arranging, setArranging] = useState(false);
+ const [failed, setFailed] = useState(null);
+ const groupingRef = useRef(grouping);
+ groupingRef.current = grouping;
+
+ const saveLayout = useCallback(() => {
+ const g = graphRef.current;
+ if (!g) return;
+ const out: Record = { ...(useWorkspace.getState().layouts.graph ?? {}) };
+ g.forEachNode((id, a) => {
+ out[id] = [Math.round(a.x * 100) / 100, Math.round(a.y * 100) / 100];
+ });
+ const entries = Object.entries(out);
+ useWorkspace
+ .getState()
+ .setLayout('graph', entries.length > 20000 ? Object.fromEntries(entries.slice(-20000)) : out);
+ }, []);
+
+ const runLayout = useCallback(
+ (fromScratch: boolean) => {
+ const g = graphRef.current;
+ if (!g || g.order === 0) return;
+ layoutRef.current?.stop();
+ if (fromScratch) {
+ const seeds = seed2d(
+ viewRef.current.nodes.map((n) => ({
+ id: n.id,
+ group:
+ groupingRef.current === 'community'
+ ? ((n.metadata.community as number | undefined) ?? null)
+ : ((n.metadata.subsystem as string | undefined) ?? n.type),
+ })),
+ );
+ g.forEachNode((id) => {
+ const p = seeds.get(id);
+ if (p) g.mergeNodeAttributes(id, { x: p[0], y: p[1] });
+ });
+ }
+ const fa2 = {
+ ...forceAtlas2.inferSettings(g),
+ barnesHutOptimize: g.order > 1200,
+ gravity: 0.5,
+ scalingRatio: 8,
+ slowDown: 6 + Math.log(g.order + 1),
+ strongGravityMode: false,
+ };
+ const finish = () => {
+ noverlap.assign(g, { maxIterations: g.order > 3000 ? 15 : 60, settings: { margin: 2, ratio: 1.1 } });
+ setArranging(false);
+ saveLayout();
+ };
+ if (reducedMotion) {
+ // No animated settling: compute the arrangement in one step.
+ forceAtlas2.assign(g, { iterations: g.order > 3000 ? 40 : g.order > 800 ? 100 : 220, settings: fa2 });
+ finish();
+ return;
+ }
+ const layout = new FA2Layout(g, { settings: fa2 });
+ layout.start();
+ setArranging(true);
+ const base = settings.graph.animationIntensity === 'standard' ? 4200 : 2400;
+ const timer = setTimeout(() => stop(), base * (g.order > 3000 ? 1.8 : 1));
+ let stopped = false;
+ const stop = () => {
+ if (stopped) return;
+ stopped = true;
+ clearTimeout(timer);
+ layout.kill();
+ if (graphRef.current === g) finish();
+ };
+ layoutRef.current = { stop };
+ },
+ [reducedMotion, settings.graph.animationIntensity, saveLayout],
+ );
+
+ // ---- create the renderer once ------------------------------------------
+ useEffect(() => {
+ if (!container.current) return;
+ const g = new Graph({ multi: true, type: 'directed' });
+ graphRef.current = g;
+ let sigma: Sigma;
+ try {
+ sigma = new Sigma(g, container.current, {
+ allowInvalidContainer: true,
+ defaultEdgeType: 'arrow',
+ renderEdgeLabels: true,
+ zIndex: true,
+ labelFont: '"Zen Kaku Gothic New", system-ui, sans-serif',
+ labelSize: 12,
+ labelWeight: '400',
+ edgeLabelFont: '"IBM Plex Mono", monospace',
+ edgeLabelSize: 10,
+ minCameraRatio: 0.015,
+ maxCameraRatio: 6,
+ stagePadding: 60,
+ nodeReducer: (id, data) => {
+ const s = stateRef.current;
+ const res = { ...data };
+ const isSelected = s.selected.has(id);
+ if (s.activeNodes && !s.activeNodes.has(id) && !isSelected) {
+ res.color = mix(data.color as string, s.palette.canvas, 0.8);
+ res.label = '';
+ res.zIndex = 0;
+ } else if (s.activeNodes) {
+ res.zIndex = 1;
+ if (s.activeNodes.size <= 60) res.forceLabel = true;
+ }
+ if (isSelected || id === s.hover) {
+ res.highlighted = true;
+ res.forceLabel = true;
+ res.zIndex = 2;
+ res.size = (data.size as number) * 1.35;
+ }
+ return res;
+ },
+ edgeReducer: (id, data) => {
+ const s = stateRef.current;
+ const res = { ...data };
+ if (s.activeEdges) {
+ if (s.activeEdges.has(id)) {
+ res.color = data.verified ? s.palette.relationActive : s.palette.relationUnverified;
+ res.size = data.verified ? 1.8 : 1;
+ res.label = `${data.relation as string}${data.verified ? '' : ' (derived)'}`;
+ res.forceLabel = s.activeEdges.size <= 40;
+ res.zIndex = 2;
+ } else {
+ res.color = mix(data.color as string, s.palette.canvas, 0.75);
+ res.label = '';
+ res.zIndex = 0;
+ }
+ } else res.label = '';
+ return res;
+ },
+ // Hover label drawn with theme colours instead of the library's white box.
+ defaultDrawNodeHover: (ctx, data, st) => {
+ const p = stateRef.current.palette;
+ const size = st.labelSize;
+ ctx.font = `${st.labelWeight} ${size}px ${st.labelFont}`;
+ const label = typeof data.label === 'string' ? data.label : '';
+ const w = label ? ctx.measureText(label).width + 12 : 0;
+ ctx.beginPath();
+ ctx.arc(data.x, data.y, data.size + 3, 0, Math.PI * 2);
+ ctx.strokeStyle = p.selection;
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+ if (!label) return;
+ ctx.fillStyle = p.elevated;
+ ctx.strokeStyle = p.line;
+ ctx.lineWidth = 1;
+ const x = data.x + data.size + 6;
+ ctx.fillRect(x, data.y - size / 2 - 4, w, size + 8);
+ ctx.strokeRect(x, data.y - size / 2 - 4, w, size + 8);
+ ctx.fillStyle = p.text;
+ ctx.fillText(label, x + 6, data.y + size / 3);
+ },
+ });
+ } catch (err) {
+ setFailed((err as Error).message);
+ return;
+ }
+ sigmaRef.current = sigma;
+ const ws = useWorkspace.getState;
+
+ sigma.on('clickNode', (e) => {
+ const ev = e.event.original as MouseEvent;
+ ws().select(e.node, { additive: ev.ctrlKey || ev.metaKey || ev.shiftKey });
+ });
+ sigma.on('doubleClickNode', (e) => {
+ e.preventSigmaDefault();
+ expandRef.current(e.node);
+ });
+ sigma.on('enterNode', (e) => {
+ ws().setHover(e.node);
+ if (container.current) container.current.style.cursor = 'pointer';
+ });
+ sigma.on('leaveNode', () => {
+ ws().setHover(null);
+ if (container.current) container.current.style.cursor = '';
+ });
+ sigma.on('clickStage', () => ws().clearSelection());
+
+ // Dragging nodes to rearrange the layout.
+ let dragged: string | null = null;
+ let moved = false;
+ sigma.on('downNode', (e) => {
+ dragged = e.node;
+ moved = false;
+ if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox());
+ });
+ sigma.getMouseCaptor().on('mousemovebody', (e) => {
+ if (!dragged) return;
+ moved = true;
+ layoutRef.current?.stop();
+ const pos = sigma.viewportToGraph(e);
+ g.mergeNodeAttributes(dragged, { x: pos.x, y: pos.y });
+ e.preventSigmaDefault();
+ e.original.preventDefault();
+ e.original.stopPropagation();
+ });
+ const release = () => {
+ if (dragged && moved) saveLayout();
+ dragged = null;
+ };
+ sigma.getMouseCaptor().on('mouseup', release);
+
+ // Minimap, redrawn at most every 120 ms.
+ let pending = false;
+ const drawMinimap = () => {
+ pending = false;
+ const c = minimap.current;
+ if (!c || g.order === 0) return;
+ const ctx = c.getContext('2d');
+ if (!ctx) return;
+ const p = stateRef.current.palette;
+ let minX = Infinity,
+ minY = Infinity,
+ maxX = -Infinity,
+ maxY = -Infinity;
+ g.forEachNode((_, a) => {
+ if (a.x < minX) minX = a.x;
+ if (a.x > maxX) maxX = a.x;
+ if (a.y < minY) minY = a.y;
+ if (a.y > maxY) maxY = a.y;
+ });
+ const pad = 8;
+ const sx = (c.width - pad * 2) / Math.max(1e-6, maxX - minX);
+ const sy = (c.height - pad * 2) / Math.max(1e-6, maxY - minY);
+ const k = Math.min(sx, sy);
+ const ox = (c.width - (maxX - minX) * k) / 2;
+ const oy = (c.height - (maxY - minY) * k) / 2;
+ const map = (x: number, y: number): [number, number] => [
+ ox + (x - minX) * k,
+ c.height - (oy + (y - minY) * k),
+ ];
+ ctx.clearRect(0, 0, c.width, c.height);
+ const step = Math.max(1, Math.floor(g.order / 2500));
+ let i = 0;
+ g.forEachNode((id, a) => {
+ if (i++ % step) return;
+ const [x, y] = map(a.x, a.y);
+ ctx.fillStyle = stateRef.current.selected.has(id) ? p.selection : p.textMuted;
+ const r = stateRef.current.selected.has(id) ? 2.5 : 1;
+ ctx.fillRect(x - r / 2, y - r / 2, r, r);
+ });
+ const dims = sigma.getDimensions();
+ const a = sigma.viewportToGraph({ x: 0, y: 0 });
+ const b = sigma.viewportToGraph({ x: dims.width, y: dims.height });
+ const [x1, y1] = map(a.x, a.y);
+ const [x2, y2] = map(b.x, b.y);
+ ctx.strokeStyle = p.selection;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
+ (c as HTMLCanvasElement & { _unmap?: (px: number, py: number) => { x: number; y: number } })._unmap = (
+ px,
+ py,
+ ) => ({ x: minX + (px - ox) / k, y: minY + (c.height - py - oy) / k });
+ };
+ sigma.on('afterRender', () => {
+ if (pending) return;
+ pending = true;
+ setTimeout(drawMinimap, 120);
+ });
+
+ return () => {
+ layoutRef.current?.stop();
+ sigma.kill();
+ sigmaRef.current = null;
+ graphRef.current = null;
+ };
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ /** Double-click: a file unfolds its symbols; any other node unfolds the files around it. */
+ const expandAround = (id: string) => {
+ const n = adj.nodes.get(id);
+ if (!n) return;
+ if (n.type === 'File' || n.type === 'Configuration') return expandFile(id);
+ const files = new Set();
+ const own = fileIdOf(n);
+ if (own) files.add(own);
+ for (const e of [...(adj.out.get(id) ?? []), ...(adj.in.get(id) ?? [])]) {
+ const other = adj.nodes.get(e.source === id ? e.target : e.source);
+ if (!other) continue;
+ if (other.type === 'File') files.add(other.id);
+ else if (isSymbolType(other.type)) {
+ const fid = fileIdOf(other);
+ if (fid) files.add(fid);
+ }
+ }
+ [...files].slice(0, 40).forEach((f) => expandFile(f, true));
+ };
+ const expandRef = useRef(expandAround);
+ expandRef.current = expandAround;
+
+ // ---- sync graph data ----------------------------------------------------
+ useEffect(() => {
+ const g = graphRef.current;
+ const sigma = sigmaRef.current;
+ if (!g || !sigma) return;
+ const saved = useWorkspace.getState().layouts.graph ?? {};
+ const wanted = new Set(view.nodes.map((n) => n.id));
+ g.forEachNode((id) => {
+ if (!wanted.has(id)) g.dropNode(id);
+ });
+ const fresh: GraphNode[] = [];
+ const scale = view.nodes.length > 4000 ? 0.75 : view.nodes.length > 1500 ? 0.9 : 1.15;
+ for (const n of view.nodes) {
+ const attrs = {
+ label: `${FAMILY_GLYPH[NODE_FAMILY[n.type]]} ${n.name}`,
+ size: NODE_SIZE[n.type] * scale + 1.2,
+ color: nodeColor(n.type, palette),
+ nodeType: n.type,
+ };
+ if (g.hasNode(n.id)) g.mergeNodeAttributes(n.id, attrs);
+ else {
+ const s = saved[n.id];
+ if (!s) fresh.push(n);
+ g.addNode(n.id, { ...attrs, x: s?.[0] ?? 0, y: s?.[1] ?? 0 });
+ }
+ }
+ // New nodes start next to something they belong to, so expansion grows outwards from the parent.
+ const firstRun = fresh.length === view.nodes.length;
+ if (fresh.length && !firstRun) {
+ fresh.forEach((n, i) => {
+ const anchor = [fileIdOf(n), n.metadata.subsystem as string | undefined].find(
+ (a) => a && a !== n.id && g.hasNode(a) && !fresh.some((f) => f.id === a),
+ );
+ const a = anchor ? g.getNodeAttributes(anchor) : { x: 0, y: 0 };
+ const t = i * 2.399963;
+ const r = 2 + Math.sqrt(i + 1) * 1.6;
+ g.mergeNodeAttributes(n.id, { x: a.x + Math.cos(t) * r, y: a.y + Math.sin(t) * r });
+ });
+ }
+ g.clearEdges();
+ for (const e of view.edges) {
+ if (!g.hasNode(e.source) || !g.hasNode(e.target)) continue;
+ const structural = STRUCTURE_EDGES.includes(e.type);
+ g.addEdgeWithKey(e.id, e.source, e.target, {
+ size: !e.verified ? 0.5 : structural ? 0.6 : 1,
+ color: mix(
+ e.verified ? palette.textMuted : palette.relationUnverified,
+ palette.canvas,
+ structural ? 0.55 : 0.3,
+ ),
+ type: structural ? 'line' : 'arrow',
+ relation: EDGE_TYPE_LABELS[e.type],
+ verified: e.verified,
+ weight: structural ? 2 : 1,
+ });
+ }
+ sigma.setSetting('hideEdgesOnMove', view.edges.length > 5000);
+ sigma.setSetting('hideLabelsOnMove', view.nodes.length > 5000);
+ if (sigma.getCustomBBox()) sigma.setCustomBBox(null);
+ if (firstRun) runLayout(true);
+ else if (fresh.length) runLayout(false);
+ else sigma.refresh();
+ }, [view, palette, runLayout]);
+
+ // ---- settings and theme -------------------------------------------------
+ useEffect(() => {
+ const sigma = sigmaRef.current;
+ if (!sigma) return;
+ sigma.setSetting('labelColor', { color: palette.text });
+ sigma.setSetting('edgeLabelColor', { color: palette.textSecondary });
+ sigma.setSetting('labelRenderedSizeThreshold', LABEL_THRESHOLD[settings.graph.labelDensity]);
+ sigma.setSetting('labelDensity', LABEL_DENSITY[settings.graph.labelDensity]);
+ stateRef.current.palette = palette;
+ sigma.refresh();
+ }, [palette, theme, settings.graph.labelDensity]);
+
+ // ---- selection, hover and highlight ------------------------------------
+ useEffect(() => {
+ const s = stateRef.current;
+ s.selected = new Set(selectedIds);
+ s.primary = primaryId;
+ s.hover = hoverId;
+ const centre = hoverId ?? primaryId;
+ if (centre && graphRef.current?.hasNode(centre)) {
+ const nb = neighbourhood(centre, view.edges);
+ for (const id of selectedIds)
+ if (id !== centre) for (const x of neighbourhood(id, view.edges).nodes) nb.nodes.add(x);
+ s.activeNodes = nb.nodes;
+ s.activeEdges = nb.edges;
+ } else if (highlight) {
+ s.activeNodes = new Set(highlight.nodeIds);
+ s.activeEdges = new Set(highlight.edgeIds);
+ } else {
+ s.activeNodes = null;
+ s.activeEdges = null;
+ }
+ sigmaRef.current?.refresh({ skipIndexation: true });
+ }, [selectedIds, primaryId, hoverId, highlight, view]);
+
+ // ---- bring the selection into view -------------------------------------
+ useEffect(() => {
+ const sigma = sigmaRef.current;
+ const id = useWorkspace.getState().primaryId ?? useWorkspace.getState().focusId;
+ if (!sigma || revealTick === 0) return;
+ const t = setTimeout(() => {
+ if (id && graphRef.current?.hasNode(id) && settings.graph.focusBehavior !== 'none') {
+ const d = sigma.getNodeDisplayData(id);
+ if (d)
+ void sigma
+ .getCamera()
+ .animate(
+ { x: d.x, y: d.y, ratio: Math.min(sigma.getCamera().ratio, 0.4) },
+ { duration: reducedMotion ? 0 : 700 },
+ );
+ } else if (!id) void sigma.getCamera().animatedReset({ duration: reducedMotion ? 0 : 600 });
+ }, 60);
+ return () => clearTimeout(t);
+ }, [revealTick, reducedMotion, settings.graph.focusBehavior]);
+
+ const fit = () => {
+ const sigma = sigmaRef.current;
+ if (!sigma) return;
+ sigma.setCustomBBox(null);
+ void sigma.getCamera().animatedReset({ duration: reducedMotion ? 0 : 500 });
+ };
+ const exportPng = () => {
+ const sigma = sigmaRef.current;
+ if (!sigma || !container.current) return;
+ sigma.once('afterRender', () => {
+ const { width, height } = sigma.getDimensions();
+ const ratio = window.devicePixelRatio || 1;
+ const out = document.createElement('canvas');
+ out.width = width * ratio;
+ out.height = height * ratio;
+ const ctx = out.getContext('2d')!;
+ ctx.fillStyle = palette.canvas;
+ ctx.fillRect(0, 0, out.width, out.height);
+ container.current!.querySelectorAll('canvas').forEach((c) => {
+ if (c !== minimap.current) ctx.drawImage(c, 0, 0, out.width, out.height);
+ });
+ downloadUrl('vectora-graph.png', out.toDataURL('image/png'));
+ });
+ sigma.refresh();
+ };
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ const sigma = sigmaRef.current;
+ if (!sigma) return;
+ const cam = sigma.getCamera();
+ const step = 0.12 * cam.ratio;
+ const dur = { duration: reducedMotion ? 0 : 160 };
+ if (e.key === '+' || e.key === '=') void cam.animatedZoom(dur);
+ else if (e.key === '-') void cam.animatedUnzoom(dur);
+ else if (e.key === '0') fit();
+ else if (e.key === 'ArrowLeft') void cam.animate({ x: cam.x - step }, dur);
+ else if (e.key === 'ArrowRight') void cam.animate({ x: cam.x + step }, dur);
+ else if (e.key === 'ArrowUp') void cam.animate({ y: cam.y + step }, dur);
+ else if (e.key === 'ArrowDown') void cam.animate({ y: cam.y - step }, dur);
+ else if (e.key === 'n' || e.key === 'p') {
+ // Step through the neighbours of the selection (or all nodes) without a pointer.
+ const ws = useWorkspace.getState();
+ const pool = ws.primaryId
+ ? [...neighbourhood(ws.primaryId, view.edges).nodes]
+ : view.nodes.map((n) => n.id);
+ if (!pool.length) return;
+ const i = pool.indexOf(ws.hoverId ?? ws.primaryId ?? '');
+ ws.setHover(pool[(i + (e.key === 'n' ? 1 : -1) + pool.length) % pool.length]!);
+ } else if (e.key === 'Enter') {
+ const ws = useWorkspace.getState();
+ if (ws.hoverId) ws.select(ws.hoverId, { reveal: true });
+ } else return;
+ e.preventDefault();
+ };
+
+ if (failed)
+ return (
+
+
The graph renderer could not start
+
+ WebGL is not available in this browser ({failed}). The Mind Map and Search views work without it.
+
+
+ );
+
+ return (
+
+
+
+
+ Fit
+
+
+ Group by
+ {
+ groupingRef.current = e.target.value as Grouping;
+ setGrouping(e.target.value as Grouping);
+ runLayout(true);
+ }}
+ >
+ subsystem
+ community
+
+
+ runLayout(true)}>
+
+ Rearrange
+
+
+
+ PNG
+
+
+
+
{
+ const sigma = sigmaRef.current;
+ const c = minimap.current as
+ (HTMLCanvasElement & { _unmap?: (px: number, py: number) => { x: number; y: number } }) | null;
+ if (!sigma || !c?._unmap) return;
+ const r = c.getBoundingClientRect();
+ const gp = c._unmap(
+ ((e.clientX - r.left) / r.width) * c.width,
+ ((e.clientY - r.top) / r.height) * c.height,
+ );
+ const framed = sigma.viewportToFramedGraph(sigma.graphToViewport(gp));
+ void sigma.getCamera().animate({ x: framed.x, y: framed.y }, { duration: reducedMotion ? 0 : 400 });
+ }}
+ />
+
+ Click to inspect, double-click to unfold, Ctrl-click to add to the selection, drag to rearrange
+
+
+ );
+}
diff --git a/apps/web/src/workspace/MindMap.tsx b/apps/web/src/workspace/MindMap.tsx
new file mode 100644
index 0000000..67e6098
--- /dev/null
+++ b/apps/web/src/workspace/MindMap.tsx
@@ -0,0 +1,637 @@
+import dagre from '@dagrejs/dagre';
+import {
+ Background,
+ Controls,
+ type Edge,
+ Handle,
+ MiniMap,
+ type Node,
+ type NodeProps,
+ Position,
+ ReactFlow,
+ ReactFlowProvider,
+ getNodesBounds,
+ getViewportForBounds,
+ useReactFlow,
+} from '@xyflow/react';
+import '@xyflow/react/dist/style.css';
+import { toPng, toSvg } from 'html-to-image';
+import { ChevronRight, Image as ImageIcon, RotateCcw } from 'lucide-react';
+import { memo, useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ EDGE_TYPES,
+ EDGE_TYPE_LABELS,
+ type EdgeType,
+ type GraphNode,
+ isSymbolType,
+} from '@vectora/graph-schema';
+import { ViewToolbar } from '@/components/ViewControls';
+import { cx } from '@/components/ui';
+import { downloadUrl } from '@/lib/api';
+import { type Adjacency, fileIdOf } from '@/lib/graphView';
+import { useWorkspace } from '@/lib/store';
+import { FAMILY_GLYPH, NODE_FAMILY } from '@/lib/tokens';
+import { useWs } from './context';
+
+interface TreeItem {
+ /** Tree-unique key. A graph node can appear under several branches (e.g. a file under a subsystem and a concept). */
+ key: string;
+ nodeId: string | null;
+ label: string;
+ node: GraphNode | null;
+ children: TreeItem[];
+ derived?: boolean;
+}
+
+const dirOf = (p: string): string => (p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '');
+
+/**
+ * Builds the conceptual hierarchy from the knowledge graph:
+ * repository → subsystems → folders → files → symbols, plus a branch of concepts.
+ */
+export function buildTree(adj: Adjacency, hiddenTypes: ReadonlySet): TreeItem | null {
+ const repo = adj.nodes.get('repo:root');
+ if (!repo) return null;
+ const symbolChildren = (parentId: string, prefix: string, depth = 0): TreeItem[] =>
+ depth > 3
+ ? []
+ : (adj.out.get(parentId) ?? [])
+ .filter((e) => e.type === 'CONTAINS')
+ .map((e) => adj.nodes.get(e.target))
+ .filter((n): n is GraphNode => !!n && isSymbolType(n.type) && !hiddenTypes.has(n.type))
+ .sort((a, b) => (a.location?.startLine ?? 0) - (b.location?.startLine ?? 0))
+ .map((n) => ({
+ key: `${prefix}>${n.id}`,
+ nodeId: n.id,
+ label: n.name,
+ node: n,
+ children: symbolChildren(n.id, prefix, depth + 1),
+ }));
+ const fileItem = (f: GraphNode, prefix: string): TreeItem => ({
+ key: `${prefix}>${f.id}`,
+ nodeId: f.id,
+ label: f.name,
+ node: f,
+ children: symbolChildren(f.id, prefix),
+ });
+
+ const subsystems = [...adj.nodes.values()]
+ .filter((n) => n.type === 'Subsystem')
+ .sort((a, b) => Number(b.metadata.files ?? 0) - Number(a.metadata.files ?? 0));
+ const subsystemItems: TreeItem[] = subsystems.map((s) => {
+ const base = String(s.filePath ?? '');
+ const files = (adj.in.get(s.id) ?? [])
+ .filter((e) => e.type === 'BELONGS_TO')
+ .map((e) => adj.nodes.get(e.source))
+ .filter((n): n is GraphNode => !!n && !hiddenTypes.has(n.type));
+ // Group files by folder below the subsystem root.
+ const root: TreeItem = { key: s.id, nodeId: s.id, label: s.name, node: s, children: [] };
+ const folders = new Map([['', root]]);
+ const folder = (rel: string): TreeItem => {
+ const existing = folders.get(rel);
+ if (existing) return existing;
+ const parent = folder(dirOf(rel));
+ const full = base ? `${base}/${rel}` : rel;
+ const dirNode = adj.nodes.get(`dir:${full}`) ?? null;
+ const item: TreeItem = {
+ key: `${s.id}>dir:${full}`,
+ nodeId: dirNode?.id ?? null,
+ label: rel.slice(rel.lastIndexOf('/') + 1),
+ node: dirNode,
+ children: [],
+ };
+ folders.set(rel, item);
+ parent.children.push(item);
+ return item;
+ };
+ for (const f of files.sort((a, b) => (a.filePath! < b.filePath! ? -1 : 1))) {
+ const rel =
+ base && f.filePath!.startsWith(`${base}/`) ? f.filePath!.slice(base.length + 1) : f.filePath!;
+ folder(dirOf(rel)).children.push(fileItem(f, s.id));
+ }
+ // A folder that only holds one folder is merged into it (`src/app` instead of `src` → `app`).
+ const squash = (item: TreeItem): TreeItem => {
+ item.children = item.children.map(squash);
+ if (
+ item !== root &&
+ item.node?.type !== 'File' &&
+ item.children.length === 1 &&
+ item.children[0]!.children.length &&
+ !item.children[0]!.node?.location
+ ) {
+ const only = item.children[0]!;
+ return { ...only, label: `${item.label}/${only.label}` };
+ }
+ return item;
+ };
+ return squash(root);
+ });
+
+ const concepts = [...adj.nodes.values()].filter((n) => n.type === 'Concept');
+ const conceptBranch: TreeItem | null = concepts.length
+ ? {
+ key: 'branch:concepts',
+ nodeId: null,
+ label: 'Concepts',
+ node: null,
+ derived: true,
+ children: concepts
+ .map((c) => ({
+ key: c.id,
+ nodeId: c.id,
+ label: c.name,
+ node: c,
+ derived: true,
+ children: (adj.in.get(c.id) ?? [])
+ .filter((e) => e.type === 'BELONGS_TO')
+ .sort((a, b) => b.confidence - a.confidence)
+ .map((e) => adj.nodes.get(e.source))
+ .filter((n): n is GraphNode => !!n)
+ .map((f) => fileItem(f, c.id)),
+ }))
+ .sort((a, b) => b.children.length - a.children.length),
+ }
+ : null;
+ return {
+ key: repo.id,
+ nodeId: repo.id,
+ label: repo.name,
+ node: repo,
+ children: [...subsystemItems, ...(conceptBranch ? [conceptBranch] : [])],
+ };
+}
+
+interface MindData extends Record {
+ item: TreeItem;
+ collapsed: boolean;
+ depth: number;
+ selected: boolean;
+ dimmed: boolean;
+ matched: boolean;
+ onToggle: (key: string) => void;
+}
+
+const MindNode = memo(function MindNode({ data }: NodeProps>) {
+ const { item, collapsed, depth, selected, dimmed, matched } = data;
+ const count = item.children.length;
+ return (
+
+
+ {item.node ? (
+
+ {FAMILY_GLYPH[NODE_FAMILY[item.node.type]]}
+
+ ) : null}
+
+ {item.label}
+
+ {count ? (
+ {
+ e.stopPropagation();
+ data.onToggle(item.key);
+ }}
+ >
+ {count}
+
+
+ ) : null}
+
+
+ );
+});
+
+const nodeTypes = { mind: MindNode };
+const NODE_H = 34;
+const widthOf = (item: TreeItem, depth: number) =>
+ Math.min(300, Math.max(128, item.label.length * (depth === 0 ? 10.5 : depth === 1 ? 9 : 7.6) + 84));
+
+function MindMapInner() {
+ const { adj, graph, palette, reveal, reducedMotion } = useWs();
+ const flow = useReactFlow();
+ const selectedIds = useWorkspace((s) => s.selectedIds);
+ const hiddenNodeTypes = useWorkspace((s) => s.hiddenNodeTypes);
+ const revealTick = useWorkspace((s) => s.revealTick);
+ const layouts = useWorkspace((s) => s.layouts);
+ const setLayout = useWorkspace((s) => s.setLayout);
+ const [query, setQuery] = useState('');
+ const [relation, setRelation] = useState('');
+ const [drag, setDrag] = useState<{ id: string; x: number; y: number } | null>(null);
+
+ // Folders are part of the hierarchy here even when hidden in the other views.
+ const hidden = useMemo(
+ () =>
+ new Set(hiddenNodeTypes.filter((t) => t !== 'Directory' && t !== 'Module' && t !== 'Configuration')),
+ [hiddenNodeTypes],
+ );
+ const tree = useMemo(() => buildTree(adj, hidden), [adj, hidden]);
+
+ // Collapsed state: everything below the second level starts collapsed.
+ const defaultCollapsed = useMemo(() => {
+ const out: string[] = [];
+ const walk = (item: TreeItem, depth: number) => {
+ if (depth >= 1 && item.children.length) out.push(item.key);
+ item.children.forEach((c) => walk(c, depth + 1));
+ };
+ if (tree) walk(tree, 0);
+ return out;
+ }, [tree]);
+ const childKeys = useMemo(() => {
+ const m = new Map();
+ const walk = (item: TreeItem) => {
+ m.set(
+ item.key,
+ item.children.map((c) => c.key),
+ );
+ item.children.forEach(walk);
+ };
+ if (tree) walk(tree);
+ return m;
+ }, [tree]);
+ const collapsedList = layouts.mindmapCollapsed ?? defaultCollapsed;
+ const collapsed = useMemo(() => new Set(collapsedList), [collapsedList]);
+ const toggle = useCallback(
+ (key: string) => {
+ const next = new Set(useWorkspace.getState().layouts.mindmapCollapsed ?? defaultCollapsed);
+ const expanding = next.has(key);
+ if (expanding) next.delete(key);
+ else next.add(key);
+ setLayout('mindmapCollapsed', [...next]);
+ // Keep a newly opened branch on screen: pan (and zoom out if needed) to show it with its children.
+ if (expanding)
+ setTimeout(() => {
+ const ids = [key, ...(childKeys.get(key) ?? []).slice(0, 60)].filter((id) => flow.getNode(id));
+ void flow.fitView({
+ nodes: ids.map((id) => ({ id })),
+ padding: 0.25,
+ maxZoom: flow.getZoom(),
+ duration: reducedMotion ? 0 : 380,
+ });
+ }, 80);
+ },
+ [defaultCollapsed, setLayout, childKeys, flow, reducedMotion],
+ );
+
+ // Paths from the root to every tree item, for search and reveal.
+ const index = useMemo(() => {
+ const pathTo = new Map();
+ const byNode = new Map();
+ const walk = (item: TreeItem, path: string[]) => {
+ pathTo.set(item.key, path);
+ if (item.nodeId)
+ (byNode.get(item.nodeId) ?? byNode.set(item.nodeId, []).get(item.nodeId)!).push(item.key);
+ item.children.forEach((c) => walk(c, [...path, item.key]));
+ };
+ if (tree) walk(tree, []);
+ return { pathTo, byNode };
+ }, [tree]);
+
+ const expandTo = useCallback(
+ (key: string) => {
+ const path = index.pathTo.get(key);
+ if (!path) return;
+ const next = new Set(useWorkspace.getState().layouts.mindmapCollapsed ?? defaultCollapsed);
+ path.forEach((k) => next.delete(k));
+ setLayout('mindmapCollapsed', [...next]);
+ },
+ [index, defaultCollapsed, setLayout],
+ );
+
+ const matches = useMemo(() => {
+ const q = query.trim().toLowerCase();
+ if (q.length < 2 || !tree) return [] as string[];
+ const out: string[] = [];
+ const walk = (item: TreeItem) => {
+ if (item.label.toLowerCase().includes(q) || item.node?.filePath?.toLowerCase().includes(q))
+ out.push(item.key);
+ item.children.forEach(walk);
+ };
+ walk(tree);
+ return out.slice(0, 200);
+ }, [query, tree]);
+
+ // ---- visible items and layout ------------------------------------------
+ const { nodes, edges } = useMemo(() => {
+ if (!tree) return { nodes: [] as Node[], edges: [] as Edge[] };
+ const g = new dagre.graphlib.Graph();
+ g.setGraph({ rankdir: 'LR', nodesep: 10, ranksep: 56, marginx: 24, marginy: 24 });
+ g.setDefaultEdgeLabel(() => ({}));
+ const visible: { item: TreeItem; depth: number; parent: string | null }[] = [];
+ const walk = (item: TreeItem, depth: number, parent: string | null) => {
+ visible.push({ item, depth, parent });
+ g.setNode(item.key, { width: widthOf(item, depth), height: depth === 0 ? 42 : NODE_H });
+ if (parent) g.setEdge(parent, item.key);
+ if (!collapsed.has(item.key)) item.children.slice(0, 250).forEach((c) => walk(c, depth + 1, item.key));
+ };
+ walk(tree, 0, null);
+ dagre.layout(g);
+ const selected = new Set(selectedIds);
+ const matchSet = new Set(matches);
+ const overrides = layouts.mindmap ?? {};
+ const rfNodes: Node[] = visible.map(({ item, depth }) => {
+ const p = g.node(item.key);
+ const o = overrides[item.key];
+ return {
+ id: item.key,
+ type: 'mind',
+ position: o ? { x: o[0], y: o[1] } : { x: p.x - p.width / 2, y: p.y - p.height / 2 },
+ data: {
+ item,
+ collapsed: collapsed.has(item.key),
+ depth,
+ selected: !!item.nodeId && selected.has(item.nodeId),
+ dimmed: matchSet.size > 0 && !matchSet.has(item.key),
+ matched: matchSet.has(item.key),
+ onToggle: toggle,
+ },
+ width: p.width,
+ height: p.height,
+ };
+ });
+ const rfEdges: Edge[] = visible
+ .filter((v) => v.parent)
+ .map((v) => ({
+ id: `tree:${v.parent}>${v.item.key}`,
+ source: v.parent!,
+ target: v.item.key,
+ type: 'smoothstep',
+ selectable: false,
+ style: { stroke: palette.ash, strokeWidth: 1, strokeDasharray: v.item.derived ? '4 4' : undefined },
+ }));
+
+ // Relationship overlay: draw the chosen relationship between visible items,
+ // lifting each end to its nearest visible ancestor.
+ if (relation) {
+ const visibleByNode = new Map();
+ for (const v of visible)
+ if (v.item.nodeId && !visibleByNode.has(v.item.nodeId)) visibleByNode.set(v.item.nodeId, v.item.key);
+ const lift = (id: string): string | null => {
+ let cur: GraphNode | undefined = adj.nodes.get(id);
+ for (let i = 0; cur && i < 6; i++) {
+ const k = visibleByNode.get(cur.id);
+ if (k) return k;
+ const parentEdge = (adj.in.get(cur.id) ?? []).find((e) => e.type === 'CONTAINS');
+ cur = parentEdge
+ ? adj.nodes.get(parentEdge.source)
+ : cur.filePath && cur.id !== fileIdOf(cur)
+ ? adj.nodes.get(fileIdOf(cur)!)
+ : undefined;
+ }
+ return null;
+ };
+ const seen = new Map();
+ for (const e of graph.edges) {
+ if (e.type !== relation) continue;
+ const a = lift(e.source);
+ const b = lift(e.target);
+ if (!a || !b || a === b) continue;
+ const k = `${a}|${b}`;
+ const cur = seen.get(k);
+ if (cur) cur.count++;
+ else seen.set(k, { count: 1, verified: e.verified });
+ if (seen.size > 400) break;
+ }
+ for (const [k, v] of seen) {
+ const [a, b] = k.split('|') as [string, string];
+ rfEdges.push({
+ id: `rel:${k}`,
+ source: a,
+ target: b,
+ type: 'default',
+ selectable: false,
+ label: v.count > 1 ? `${EDGE_TYPE_LABELS[relation]} ×${v.count}` : EDGE_TYPE_LABELS[relation],
+ labelStyle: { fontFamily: 'IBM Plex Mono, monospace', fontSize: 10, fill: palette.textSecondary },
+ labelBgStyle: { fill: palette.canvas },
+ style: {
+ stroke: v.verified ? palette.relationActive : palette.relationUnverified,
+ strokeWidth: 1.2,
+ strokeDasharray: v.verified ? undefined : '5 4',
+ },
+ markerEnd: {
+ type: 'arrowclosed' as never,
+ color: v.verified ? palette.relationActive : palette.relationUnverified,
+ width: 14,
+ height: 14,
+ },
+ zIndex: 5,
+ });
+ }
+ }
+ return { nodes: rfNodes, edges: rfEdges };
+ }, [tree, collapsed, selectedIds, matches, layouts.mindmap, palette, relation, adj, graph.edges, toggle]);
+
+ // While a branch is being dragged only its position changes; the layout is not recomputed.
+ const displayNodes = useMemo(
+ () =>
+ drag ? nodes.map((n) => (n.id === drag.id ? { ...n, position: { x: drag.x, y: drag.y } } : n)) : nodes,
+ [nodes, drag],
+ );
+
+ // ---- focus: bring a node into view -------------------------------------
+ const centreOn = useCallback(
+ (key: string) => {
+ expandTo(key);
+ setTimeout(() => {
+ const n = flow.getNode(key);
+ if (n)
+ void flow.setCenter(n.position.x + (n.width ?? 120) / 2, n.position.y + NODE_H / 2, {
+ zoom: Math.max(flow.getZoom(), 0.9),
+ duration: reducedMotion ? 0 : 420,
+ });
+ }, 60);
+ },
+ [expandTo, flow, reducedMotion],
+ );
+ useEffect(() => {
+ if (revealTick === 0) return;
+ const id = useWorkspace.getState().primaryId;
+ const key = id ? index.byNode.get(id)?.[0] : null;
+ if (key) centreOn(key);
+ }, [revealTick]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const exportImage = async (kind: 'png' | 'svg') => {
+ const el = document.querySelector('.react-flow__viewport');
+ if (!el) return;
+ const bounds = getNodesBounds(flow.getNodes());
+ const width = Math.min(8000, Math.max(800, bounds.width + 160));
+ const height = Math.min(8000, Math.max(500, bounds.height + 160));
+ const vp = getViewportForBounds(bounds, width, height, 0.2, 2, 0.05);
+ const options = {
+ backgroundColor: palette.canvas,
+ width,
+ height,
+ // Fonts are bundled with the application; skipping remote font inlining keeps the export offline.
+ skipFonts: true,
+ style: {
+ width: `${width}px`,
+ height: `${height}px`,
+ transform: `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`,
+ },
+ };
+ const url = kind === 'png' ? await toPng(el, options) : await toSvg(el, options);
+ downloadUrl(`vectora-mindmap.${kind}`, url);
+ };
+
+ if (!tree) return null;
+ return (
+
+
+ setQuery(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' && matches[0]) {
+ centreOn(matches[0]);
+ const item = nodes.find((n) => n.id === matches[0])?.data.item;
+ if (item?.nodeId) useWorkspace.getState().select(item.nodeId);
+ }
+ }}
+ />
+ {query.trim().length >= 2 ? (
+ {matches.length} found — Enter focuses the first
+ ) : null}
+
+ Show relationship
+ setRelation(e.target.value as EdgeType | '')}
+ data-testid="mind-relation"
+ >
+ none
+ {EDGE_TYPES.filter(
+ (t) =>
+ !['CONTAINS', 'DEFINED_IN', 'BELONGS_TO', 'EXPORTS'].includes(t) &&
+ (graph.stats.edgesByType[t] ?? 0) > 0,
+ ).map((t) => (
+
+ {EDGE_TYPE_LABELS[t]}
+
+ ))}
+
+
+ setLayout('mindmapCollapsed', [])}>
+ Expand all
+
+ {
+ setLayout('mindmapCollapsed', undefined);
+ setLayout('mindmap', undefined);
+ setTimeout(() => void flow.fitView({ duration: reducedMotion ? 0 : 400 }), 60);
+ }}
+ >
+
+ Reset layout
+
+ void exportImage('svg')}
+ data-testid="mind-export-svg"
+ >
+
+ SVG
+
+ void exportImage('png')}>
+ PNG
+
+
+ 600}
+ proOptions={{ hideAttribution: false }}
+ onNodeClick={(e, n) => {
+ const item = (n.data as MindData).item;
+ if (item.nodeId)
+ useWorkspace.getState().select(item.nodeId, { additive: e.ctrlKey || e.metaKey || e.shiftKey });
+ }}
+ onNodeDoubleClick={(_e, n) => {
+ const item = (n.data as MindData).item;
+ if (item.children.length) toggle(item.key);
+ else if (item.nodeId) reveal(item.nodeId);
+ }}
+ onNodeDrag={(_e, n) => setDrag({ id: n.id, x: n.position.x, y: n.position.y })}
+ onNodeDragStop={(_e, n) => {
+ setDrag(null);
+ // Rearranged branches keep their position for this workspace.
+ setLayout('mindmap', {
+ ...(useWorkspace.getState().layouts.mindmap ?? {}),
+ [n.id]: [Math.round(n.position.x), Math.round(n.position.y)],
+ });
+ }}
+ onNodesChange={() => undefined}
+ onPaneClick={() => useWorkspace.getState().clearSelection()}
+ aria-label="Hierarchical mind map of the repository"
+ >
+
+
+
+
+
+ );
+}
+
+export function MindMap() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/src/workspace/Overview.tsx b/apps/web/src/workspace/Overview.tsx
new file mode 100644
index 0000000..5a0ce84
--- /dev/null
+++ b/apps/web/src/workspace/Overview.tsx
@@ -0,0 +1,279 @@
+import { Empty, Loading, SectionHeading } from '@/components/ui';
+import { useOverview } from '@/lib/api';
+import { useWorkspace } from '@/lib/store';
+import { useWs } from './context';
+
+export function Overview() {
+ const { repo, reveal, openView, settings } = useWs();
+ const { data: o, isLoading, error } = useOverview(repo.id);
+ const setFocus = useWorkspace((s) => s.setFocus);
+ if (isLoading) return ;
+ if (error || !o)
+ return {(error as Error | null)?.message} ;
+
+ const show = (id: string, view: 'graph' | 'atlas' | 'mindmap' = settings.graph.defaultMode) => {
+ reveal(id);
+ openView(view);
+ };
+ const analysed = o.languages.filter((l) => l.tier > 0);
+ const other = o.languages.filter((l) => l.tier === 0);
+ const maxFiles = Math.max(1, ...analysed.map((l) => l.files));
+
+ return (
+
+
+ {/* Lead column, deliberately narrow and offset */}
+
+ {repo.sourceKind === 'sample' ? 'Bundled sample repository' : repo.origin}
+ {repo.name}
+ {o.description ? (
+
+ {o.description} — {o.descriptionSource}
+
+ ) : (
+ No description was found in the manifests or the README.
+ )}
+
+ openView('graph')}>
+ Open the Graph Explorer
+
+ openView('atlas')}>
+ Open the 3D Atlas
+
+ openView('mindmap')}>
+ Open the Mind Map
+
+
+
+
+
+
+ {[
+ ['Files', o.repository.fileCount],
+ ['Symbols', o.repository.symbolCount],
+ ['Relationships', o.repository.relationshipCount],
+ ['Unresolved references', o.unresolved],
+ ].map(([k, v]) => (
+
+
{k}
+
+ {(v as number).toLocaleString()}
+
+
+ ))}
+
+
+ {o.summary
+ ? `Last run: ${o.summary.analyzedFiles} files analysed, ${o.summary.reusedFiles} reused from cache, ${(o.summary.durationMs / 1000).toFixed(1)} s${o.summary.incremental ? ' (incremental)' : ''}.`
+ : 'No indexing summary is stored.'}{' '}
+ Graph checks: {o.validation.errors} errors, {o.validation.warnings} warnings.
+
+ {o.unresolved > 0 ? (
+
+ Unresolved references are listed in the Dependency Explorer. They are never drawn as
+ relationships.
+
+ ) : null}
+
+
+
+ Architecture summary
+
+
+
+
+ Languages
+
+ {analysed.map((l) => (
+
+
+ {l.language}
+
+ {l.analyzedFiles}/{l.files} files, {l.symbols.toLocaleString()} symbols
+
+
+
+
+ {l.adapter ?? 'not analysed'}
+ {l.parseErrors ? `, ${l.parseErrors} parse notes` : ''}
+
+
+ ))}
+
+ {other.length ? (
+
+ Detected but not analysed at symbol level:{' '}
+ {other.map((l) => `${l.language} (${l.files})`).join(', ')}.
+
+ ) : null}
+ {o.frameworks.length ? (
+ <>
+ Frameworks
+
+ {o.frameworks.map((f) => (
+
+ {f.name}
+
+ ))}
+
+ >
+ ) : null}
+
+
+
+
+ Major subsystems
+
+
+
+
+ Subsystem
+ Files
+ Symbols
+
+ Out
+
+
+ In
+
+
+
+
+ {o.subsystems.map((s) => (
+
+
+ {
+ setFocus(null);
+ show(s.id);
+ }}
+ >
+ {s.name}
+
+
+ {s.files}
+ {s.symbols}
+ {s.outbound}
+ {s.inbound}
+
+ ))}
+
+
+
+
+
+ Main entry points
+ {o.entryPoints.length === 0 ? (
+ No entry point could be identified from manifests or file conventions.
+ ) : null}
+ {o.entryPoints.map((e) => (
+ show(e.nodeId)}>
+
+ {e.path}
+ {e.reason}
+
+
+ ))}
+ {o.concepts.length ? (
+ <>
+
+ Concepts
+
+ {o.concepts.map((c) => (
+ show(c.id, 'mindmap')}
+ title={c.evidence.join('\n')}
+ >
+ {c.name}
+ {c.members} files
+
+ ))}
+ >
+ ) : null}
+
+
+
+ Largest modules
+
+
+
+ File
+ Symbols
+ Lines
+
+
+
+ {o.largestModules.map((m) => (
+
+
+ show(m.id)}
+ title={m.path}
+ >
+ {m.path}
+
+
+ {m.symbols}
+ {m.lines.toLocaleString()}
+
+ ))}
+
+
+
+
+
+ Dependency statistics
+
+ {[
+ ['Declared', o.dependencies.declared],
+ ['Imported', o.dependencies.imported],
+ ['Internal imports', o.dependencies.internalImports],
+ ].map(([k, v]) => (
+
+
{k}
+ {(v as number).toLocaleString()}
+
+ ))}
+
+
+ {o.dependencies.externalPackages
+ .filter((p) => p.importers > 0)
+ .slice(0, 8)
+ .map((p) => (
+
+ {p.name}
+
+ {p.importers} file{p.importers === 1 ? '' : 's'}
+ {p.declared ? '' : ', not declared'}
+
+
+ ))}
+
+
+ openView('dependencies')}>
+ Open the Dependency Explorer
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/workspace/SearchView.tsx b/apps/web/src/workspace/SearchView.tsx
new file mode 100644
index 0000000..ef3b541
--- /dev/null
+++ b/apps/web/src/workspace/SearchView.tsx
@@ -0,0 +1,309 @@
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { useSearchParams } from 'react-router';
+import { NODE_TYPES, NODE_TYPE_LABELS, type NodeType } from '@vectora/graph-schema';
+import type { ExplorationAnswer } from '@vectora/shared';
+import { Notice, TypeMark, cx } from '@/components/ui';
+import { api } from '@/lib/api';
+import { useWorkspace } from '@/lib/store';
+import { useWs } from './context';
+
+const EXAMPLES = [
+ 'Explain authentication',
+ 'Identify circular dependencies',
+ 'Explain the application startup sequence',
+ 'Show the complete database access path',
+];
+
+export function SearchView() {
+ const { repo, graph, reveal, openView, settings } = useWs();
+ const [params, setParams] = useSearchParams();
+ const mode = params.get('mode') === 'questions' ? 'questions' : 'search';
+ const [input, setInput] = useState(params.get('q') ?? '');
+ const [query, setQuery] = useState(input.trim());
+ const [types, setTypes] = useState([]);
+ const [question, setQuestion] = useState('');
+ const setHighlight = useWorkspace((s) => s.setHighlight);
+ useEffect(() => {
+ const t = setTimeout(() => setQuery(input.trim()), 180);
+ return () => clearTimeout(t);
+ }, [input]);
+ const search = useQuery({
+ queryKey: ['search', repo.id, query, types.join(',')],
+ queryFn: () => api.search(repo.id, query, types),
+ enabled: mode === 'search' && query.length >= 2,
+ placeholderData: (p) => p,
+ });
+ const ask = useMutation({ mutationFn: (q: string) => api.ask(repo.id, q) });
+ const answer: ExplorationAnswer | undefined = ask.data;
+
+ const present = NODE_TYPES.filter((t) => (graph.stats.nodesByType[t] ?? 0) > 0 && t !== 'Repository');
+ const open = (id: string) => {
+ reveal(id);
+ openView(settings.graph.defaultMode);
+ };
+ const submit = (q: string) => {
+ const text = q.trim();
+ if (text.length < 2) return;
+ setQuestion(text);
+ ask.mutate(text);
+ };
+
+ return (
+
+
+
Search
+
+ {(
+ [
+ ['search', 'Find by name or path'],
+ ['questions', 'Ask about the repository'],
+ ] as const
+ ).map(([id, label]) => (
+ setParams(id === 'questions' ? { mode: 'questions' } : {})}
+ >
+ {label}
+
+ ))}
+
+
+
+ {mode === 'search' ? (
+
+
+
setInput(e.target.value)}
+ autoFocus
+ data-testid="search-input"
+ />
+
+ {present.map((t) => (
+
+ setTypes((cur) => (cur.includes(t) ? cur.filter((x) => x !== t) : [...cur, t]))
+ }
+ >
+ {NODE_TYPE_LABELS[t]}
+
+ ))}
+
+ {search.error ? (
+
+ {(search.error as Error).message}
+
+ ) : null}
+ {query.length < 2 ? (
+
+ Type at least two characters. Exact names rank first, then prefixes, partial matches, paths
+ and descriptions.
+
+ ) : search.data ? (
+ <>
+
+ {search.data.results.length} graph results
+ {search.data.documents.length
+ ? `, ${search.data.documents.length} documentation matches`
+ : ''}{' '}
+ for “{search.data.query}”
+
+
+ {search.data.results.map((r) => (
+
+ open(r.id)} data-testid="search-result">
+
+ {r.name}
+
+ {r.filePath
+ ? `${r.filePath}${r.line && r.type !== 'File' ? `:${r.line}` : ''}`
+ : (r.description ?? '')}
+ {r.match !== 'name' ? ` — matched on ${r.match}` : ''}
+
+
+
+
+
+ ))}
+
+ {search.data.results.length === 0 && search.data.documents.length === 0 ? (
+
+ Nothing matches. Check the spelling, or search for part of a file path.
+
+ ) : null}
+ {search.data.documents.length ? (
+ <>
+
Documentation
+
+ {search.data.documents.map((d) => (
+
+ openView('docs', `path=${encodeURIComponent(d.path)}`)}
+ >
+
+ {d.snippet}
+
+ {d.path}:{d.line}
+ {d.heading ? ` — ${d.heading}` : ''}
+
+
+
+
+ ))}
+
+ >
+ ) : null}
+ >
+ ) : null}
+
+
+ ) : (
+
+
+
+ Questions are answered from the knowledge graph. Every statement cites the file and line it
+ comes from, and anything the graph cannot establish is listed separately.
+
+
+
+ {EXAMPLES.map((e) => (
+ submit(e)}>
+ {e}
+
+ ))}
+
+ {ask.error ? (
+
+ {(ask.error as Error).message}
+
+ ) : null}
+ {answer ? (
+
+ {question}
+
+
{answer.title}
+ {answer.nodeIds.length ? (
+ {
+ setHighlight({
+ label: answer.title,
+ nodeIds: answer.nodeIds,
+ edgeIds: answer.edgeIds,
+ });
+ useWorkspace.getState().clearSelection();
+ openView(
+ settings.graph.defaultMode === 'mindmap' ? 'graph' : settings.graph.defaultMode,
+ );
+ }}
+ >
+ Highlight {answer.nodeIds.length} nodes in the graph
+
+ ) : null}
+
+ {answer.narrative ? (
+
+
{answer.narrative}
+
+ Summary written by the optional narrative service at {answer.narrativeSource}. The
+ evidence below comes from the graph.
+
+
+ ) : null}
+
+ {answer.statements.map((s, i) => (
+
+
+ {' '}
+ {!s.verified ? derived : null}
+
+ {s.evidence.length ? (
+
+ {s.evidence.slice(0, 4).map((ev, j) => (
+
+ openView('source', `path=${encodeURIComponent(ev.path)}&line=${ev.startLine}`)
+ }
+ >
+ {ev.path}:{ev.startLine}
+
+ ))}
+
+ ) : null}
+
+ ))}
+
+ {answer.caveats.length ? (
+
+
Not established
+
+ {answer.caveats.map((c, i) => (
+ {c}
+ ))}
+
+
+ ) : null}
+
+ ) : null}
+
+
+ )}
+
+ );
+}
+
+/** Renders `code` spans from statement text. The text is inserted as text nodes only. */
+function Formatted({ text }: { text: string }) {
+ const parts = text.split(/(`[^`]+`)/g);
+ return (
+ <>
+ {parts.map((p, i) =>
+ p.startsWith('`') && p.endsWith('`') ? (
+
+ {p.slice(1, -1)}
+
+ ) : (
+ {p}
+ ),
+ )}
+ >
+ );
+}
diff --git a/apps/web/src/workspace/SettingsView.tsx b/apps/web/src/workspace/SettingsView.tsx
new file mode 100644
index 0000000..df9cbaa
--- /dev/null
+++ b/apps/web/src/workspace/SettingsView.tsx
@@ -0,0 +1,527 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { type ReactNode, useEffect, useState } from 'react';
+import { EDGE_TYPES, EDGE_TYPE_LABELS } from '@vectora/graph-schema';
+import { DEFAULT_IGNORE_PATTERNS, type Settings, formatBytes } from '@vectora/shared';
+import { Dialog, Loading, Notice, NumberField, SelectField, Toggle } from '@/components/ui';
+import { api, useHealth, useSettings } from '@/lib/api';
+import { useWorkspace } from '@/lib/store';
+
+function Group({ title, intro, children }: { title: string; intro?: string; children: ReactNode }) {
+ const id = `settings-${title.toLowerCase().replace(/\s+/g, '-')}`;
+ return (
+
+
+
+ {title}
+
+ {intro ?
{intro}
: null}
+
+ {children}
+
+ );
+}
+
+export function SettingsView() {
+ const { settings, update, error } = useSettings();
+ const health = useHealth();
+ const qc = useQueryClient();
+ const data = useQuery({ queryKey: ['data-info'], queryFn: api.dataInfo });
+ const credentials = useQuery({ queryKey: ['credentials'], queryFn: api.credentials });
+ const [ignoreText, setIgnoreText] = useState(null);
+ const [confirmDelete, setConfirmDelete] = useState(false);
+ const [message, setMessage] = useState(null);
+ const [endpoint, setEndpoint] = useState(null);
+ const [model, setModel] = useState(null);
+ useEffect(() => {
+ if (!message) return;
+ const t = setTimeout(() => setMessage(null), 4000);
+ return () => clearTimeout(t);
+ }, [message]);
+ if (!settings) return ;
+
+ const set = (section: K, values: Partial) =>
+ update({ [section]: values } as Record>);
+ const hasCredential = (name: string) => credentials.data?.some((c) => c.name === name) ?? false;
+ const saveCredential = async (name: string, value: string) => {
+ await api.setCredential(name, value);
+ await qc.invalidateQueries({ queryKey: ['credentials'] });
+ setMessage('Credential saved. It is stored encrypted on this machine and is never shown again.');
+ };
+ const removeCredential = async (name: string) => {
+ await api.deleteCredential(name);
+ await qc.invalidateQueries({ queryKey: ['credentials'] });
+ setMessage('Credential removed.');
+ };
+ const toggleRelationship = (t: string) => {
+ const hidden = settings.graph.hiddenRelationships.includes(t)
+ ? settings.graph.hiddenRelationships.filter((x) => x !== t)
+ : [...settings.graph.hiddenRelationships, t];
+ set('graph', { hiddenRelationships: hidden });
+ // Apply to the open workspace as well, so the change is visible straight away.
+ const ws = useWorkspace.getState();
+ if (ws.repoId)
+ ws.setHiddenEdgeTypes(
+ hidden.includes(t)
+ ? [...new Set([...ws.hiddenEdgeTypes, t])]
+ : ws.hiddenEdgeTypes.filter((x) => x !== t),
+ );
+ };
+ const ignoreValue = ignoreText ?? settings.repository.ignorePatterns.join('\n');
+
+ return (
+
+
+
Settings
+
Changes are saved as you make them and are kept by the local engine.
+
+ {error ? (
+ {(error as Error).message}
+ ) : message ? (
+ {message}
+ ) : null}
+
+
+
+ set('appearance', { theme })}
+ options={[
+ { value: 'light', label: 'Light' },
+ { value: 'dark', label: 'Dark' },
+ { value: 'system', label: 'Follow the system' },
+ ]}
+ />
+
+
+
+ set('graph', { defaultMode })}
+ options={[
+ { value: 'graph', label: 'Graph Explorer (2D)' },
+ { value: 'atlas', label: '3D Atlas' },
+ { value: 'mindmap', label: 'Mind Map' },
+ ]}
+ />
+ set('graph', { nodeDensity })}
+ options={[
+ { value: 'structure', label: 'Structure' },
+ { value: 'balanced', label: 'Balanced' },
+ { value: 'full', label: 'Full' },
+ ]}
+ />
+ set('graph', { labelDensity })}
+ options={[
+ { value: 'low', label: 'Few labels' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'high', label: 'Many labels' },
+ ]}
+ />
+ set('graph', { animationIntensity })}
+ options={[
+ { value: 'none', label: 'None' },
+ { value: 'subtle', label: 'Subtle' },
+ { value: 'standard', label: 'Standard' },
+ ]}
+ />
+ set('graph', { focusBehavior })}
+ options={[
+ { value: 'neighbourhood', label: 'Move to the node, focus shows two steps' },
+ { value: 'node', label: 'Move to the node, focus shows one step' },
+ { value: 'none', label: 'Do not move the camera' },
+ ]}
+ />
+ set('graph', { showUnverified })}
+ />
+
+ Relationship visibility
+
+ Unchecked relationships are hidden by default in new workspaces and in the open one.
+
+
+ {EDGE_TYPES.map((t) => (
+
+ toggleRelationship(t)}
+ />
+ {EDGE_TYPE_LABELS[t]}
+
+ ))}
+
+
+
+
+
+ set('performance', { workerConcurrency })}
+ />
+ set('performance', { indexingCache })}
+ />
+ set('performance', { renderingQuality })}
+ options={[
+ { value: 'low', label: 'Low' },
+ { value: 'balanced', label: 'Balanced' },
+ { value: 'high', label: 'High' },
+ ]}
+ />
+ set('performance', { reducedMotion })}
+ options={[
+ { value: 'system', label: 'Follow the system' },
+ { value: 'on', label: 'Always reduce motion' },
+ { value: 'off', label: 'Allow motion' },
+ ]}
+ />
+ set('performance', { maxFiles })}
+ />
+ set('performance', { maxGraphNodes })}
+ />
+ {
+ await api.clearCache();
+ setMessage('Indexing cache cleared. The next index of each repository analyses every file.');
+ }}
+ >
+ Clear the indexing cache
+
+
+
+
+
+ Ignore patterns
+
+
+ One pattern per line, in .gitignore syntax. Each repository's own .gitignore files are always
+ respected as well.
+
+
+
+
+
+ Data folder
+ {data.data?.dataDir ?? '…'}
+ Database
+ {data.data ? formatBytes(data.data.databaseBytes) : '…'}
+ Imported sources
+
+ {data.data
+ ? `${formatBytes(data.data.sourcesBytes)} (copies of GitHub and ZIP imports; local folders are read in place)`
+ : '…'}
+
+ Repositories
+ {data.data?.repositories ?? '…'}
+
+
+ Credential management
+ saveCredential('github', v)}
+ onRemove={() => removeCredential('github')}
+ />
+
+ Export settings
+ set('privacy', { includeSourceInExports })}
+ />
+
+ Optional narrative service
+
+ Off by default. When on, answers in “Ask about the repository” are additionally rewritten as prose
+ by an external service you choose. Only the findings already shown to you (symbol names, file
+ paths and short evidence lines) are sent — never whole files.
+
+ set('exploration', { providerEnabled })}
+ />
+ {settings.exploration.providerEnabled ? (
+
+
+ Endpoint (HTTPS, chat-completions format)
+ setEndpoint(e.target.value)}
+ onBlur={() =>
+ endpoint !== null && set('exploration', { providerEndpoint: endpoint.trim() })
+ }
+ />
+
+
+ Model name
+ setModel(e.target.value)}
+ onBlur={() => model !== null && set('exploration', { providerModel: model.trim() })}
+ />
+
+ saveCredential('exploration-provider', v)}
+ onRemove={() => removeCredential('exploration-provider')}
+ />
+
+ ) : null}
+
+ Data deletion
+
+ Removes every repository, graph, cached analysis, saved session, stored credential and imported
+ source copy from this machine. Your own local folders are not touched.
+
+ setConfirmDelete(true)}
+ data-testid="delete-all"
+ >
+ Delete all VECTORA data
+
+
+
+
+
+
+ {data.data?.repositories ?? 0} repositories, their graphs and sessions,{' '}
+ {data.data?.credentials ?? 0} stored credentials and all imported source copies will be removed from{' '}
+ {data.data?.dataDir}.
+
+
+ setConfirmDelete(false)}>
+ Keep my data
+
+ {
+ try {
+ await api.deleteAllData();
+ await qc.invalidateQueries();
+ setConfirmDelete(false);
+ window.location.assign('/import');
+ } catch (e) {
+ setConfirmDelete(false);
+ setMessage((e as Error).message);
+ }
+ }}
+ >
+ Delete all data
+
+
+
+
+ );
+}
+
+function CredentialRow({
+ label,
+ hint,
+ stored,
+ onSave,
+ onRemove,
+}: {
+ label: string;
+ hint: string;
+ stored: boolean;
+ onSave: (v: string) => Promise;
+ onRemove: () => Promise;
+}) {
+ const [value, setValue] = useState('');
+ const id = `cred-${label.toLowerCase().replace(/\s+/g, '-')}`;
+ return (
+
+
+ {label}{' '}
+ {stored ? stored : not set }
+
+
{hint}
+
+ {/* The value is write-only: it is never read back from the engine. */}
+ setValue(e.target.value)}
+ />
+ {
+ await onSave(value.trim());
+ setValue('');
+ }}
+ >
+ Save
+
+ {stored ? (
+ void onRemove()}>
+ Remove
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/src/workspace/WorkspaceLayout.tsx b/apps/web/src/workspace/WorkspaceLayout.tsx
new file mode 100644
index 0000000..c4e01a5
--- /dev/null
+++ b/apps/web/src/workspace/WorkspaceLayout.tsx
@@ -0,0 +1,482 @@
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { Download, Moon, PanelRight, RefreshCw, Search, Sun } from 'lucide-react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { Link, NavLink, Outlet, useLocation, useNavigate, useParams } from 'react-router';
+import { INDEX_STAGES, INDEX_STAGE_LABELS } from '@vectora/shared';
+import { CommandPalette } from '@/components/CommandPalette';
+import { ExportDialog } from '@/components/ExportDialog';
+import { Inspector } from '@/components/Inspector';
+import { Empty, IconButton, Loading, Notice, Wordmark, cx } from '@/components/ui';
+import { ApiError, api, useGraph, useJob, useRepository, useSettings } from '@/lib/api';
+import { buildAdjacency, deriveView, fileIdOf } from '@/lib/graphView';
+import { useWorkspace } from '@/lib/store';
+import { usePalette, useReducedMotion, useTheme } from '@/lib/theme';
+import { WorkspaceContext, type WorkspaceContextValue } from './context';
+
+const NAV: { to: string; label: string }[] = [
+ { to: '', label: 'Overview' },
+ { to: 'mindmap', label: 'Mind Map' },
+ { to: 'graph', label: 'Graph Explorer' },
+ { to: 'atlas', label: '3D Atlas' },
+ { to: 'dependencies', label: 'Dependency Explorer' },
+ { to: 'search', label: 'Search' },
+ { to: 'docs', label: 'Documentation' },
+ { to: 'settings', label: 'Settings' },
+];
+
+const formatTime = (iso: string | null): string => {
+ if (!iso) return 'never';
+ const d = new Date(iso);
+ return `${d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} ${d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })}`;
+};
+
+export function WorkspaceLayout() {
+ const { repoId = '' } = useParams();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const qc = useQueryClient();
+ const { settings, update } = useSettings();
+ const theme = useTheme(settings?.appearance.theme);
+ const palette = usePalette(theme);
+ const reducedMotion = useReducedMotion(settings);
+ const repoQuery = useRepository(repoId);
+ const repo = repoQuery.data;
+ const ready = repo?.status === 'ready';
+ const {
+ graph,
+ partial,
+ loadMembers,
+ error: graphError,
+ } = useGraph(
+ repoId,
+ repo?.symbolCount ?? 0,
+ settings?.performance.maxGraphNodes ?? 8000,
+ !!repo && ready && !!settings,
+ );
+
+ const ws = useWorkspace();
+ const [jobId, setJobId] = useState(null);
+ const [exportOpen, setExportOpen] = useState(false);
+ const job = useJob(jobId).data;
+ const viewName = location.pathname.split('/')[3] ?? 'overview';
+
+ // ---- session restore and autosave --------------------------------------
+ const sessionQuery = useQuery({
+ queryKey: ['session', repoId],
+ queryFn: () => api.session(repoId),
+ enabled: !!repo && !!settings,
+ staleTime: Infinity,
+ gcTime: 0,
+ });
+ const hydrated = ws.sessionLoaded && ws.repoId === repoId;
+ useEffect(() => {
+ if (
+ sessionQuery.isSuccess &&
+ settings &&
+ !(useWorkspace.getState().sessionLoaded && useWorkspace.getState().repoId === repoId)
+ )
+ useWorkspace
+ .getState()
+ .hydrate(repoId, sessionQuery.data, {
+ hiddenEdgeTypes: [...new Set([...settings.graph.hiddenRelationships, 'EXPORTS'])],
+ });
+ }, [sessionQuery.isSuccess, sessionQuery.data, settings, repoId]);
+ useEffect(() => () => useWorkspace.setState({ sessionLoaded: false, repoId: null }), [repoId]);
+ const saveTimer = useRef | null>(null);
+ useEffect(() => {
+ if (!hydrated) return;
+ if (saveTimer.current) clearTimeout(saveTimer.current);
+ saveTimer.current = setTimeout(
+ () => void api.saveSession(repoId, useWorkspace.getState().snapshot(viewName)).catch(() => undefined),
+ 1200,
+ );
+ return () => {
+ if (saveTimer.current) clearTimeout(saveTimer.current);
+ };
+ }, [
+ hydrated,
+ repoId,
+ viewName,
+ ws.selectedIds,
+ ws.focusId,
+ ws.expandedIds,
+ ws.hiddenNodeTypes,
+ ws.hiddenEdgeTypes,
+ ws.layouts,
+ ]);
+
+ // ---- re-index -----------------------------------------------------------
+ useEffect(() => {
+ if (job && job.status !== 'running') {
+ void qc.invalidateQueries({ queryKey: ['repository', repoId] });
+ for (const key of ['graph', 'overview', 'analysis', 'members', 'source'])
+ void qc.invalidateQueries({ queryKey: [key, repoId] });
+ if (job.status === 'completed') setJobId(null);
+ }
+ }, [job, qc, repoId]);
+ const reindex = async () => {
+ try {
+ setJobId((await api.reindex(repoId)).id);
+ void qc.invalidateQueries({ queryKey: ['repository', repoId] });
+ } catch (e) {
+ alert((e as Error).message);
+ }
+ };
+
+ // ---- keyboard -----------------------------------------------------------
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
+ e.preventDefault();
+ useWorkspace.getState().setPaletteOpen(!useWorkspace.getState().paletteOpen);
+ } else if (e.key === 'Escape') {
+ const s = useWorkspace.getState();
+ if (s.paletteOpen) return; // the dialog closes itself
+ if (document.querySelector('[role="dialog"]')) return;
+ if (s.focusId) s.setFocus(null);
+ else if (s.highlight) s.setHighlight(null);
+ }
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, []);
+
+ // ---- shared graph view --------------------------------------------------
+ const adj = useMemo(() => (graph ? buildAdjacency(graph) : null), [graph]);
+ const view = useMemo(() => {
+ if (!graph || !adj || !settings) return null;
+ return deriveView(graph, adj, {
+ density: settings.graph.nodeDensity,
+ expandedIds: ws.expandedIds,
+ hiddenNodeTypes: ws.hiddenNodeTypes,
+ hiddenEdgeTypes: ws.hiddenEdgeTypes,
+ showUnverified: settings.graph.showUnverified,
+ focusId: ws.focusId,
+ focusDepth: settings.graph.focusBehavior === 'node' ? 1 : 2,
+ maxNodes: settings.performance.maxGraphNodes,
+ pinnedIds: [...ws.selectedIds, ...(ws.highlight?.nodeIds ?? [])],
+ });
+ }, [
+ graph,
+ adj,
+ settings,
+ ws.expandedIds,
+ ws.hiddenNodeTypes,
+ ws.hiddenEdgeTypes,
+ ws.focusId,
+ ws.selectedIds,
+ ws.highlight,
+ ]);
+
+ const expandFile = useCallback(
+ (fileId: string, force?: boolean) => {
+ const node = adj?.nodes.get(fileId);
+ if (node?.filePath && partial) void loadMembers(node.filePath);
+ useWorkspace.getState().toggleExpanded(fileId, force);
+ },
+ [adj, partial, loadMembers],
+ );
+ const openView = useCallback(
+ (v, query) => navigate(`/w/${repoId}/${v === 'overview' ? '' : v}${query ? `?${query}` : ''}`),
+ [navigate, repoId],
+ );
+ const reveal = useCallback(
+ (nodeId: string) => {
+ const n = adj?.nodes.get(nodeId);
+ const fid = n ? fileIdOf(n) : null;
+ if (n && fid && fid !== nodeId) expandFile(fid, true);
+ useWorkspace.getState().select(nodeId, { reveal: true });
+ },
+ [adj, expandFile],
+ );
+
+ // ---- render -------------------------------------------------------------
+ if (repoQuery.error)
+ return (
+
+
+ {(repoQuery.error as Error).message}
+
+ Open another repository
+
+
+
+ );
+ if (!repo || !settings) return ;
+
+ const indexing = repo.status === 'indexing' || repo.status === 'pending' || job?.status === 'running';
+ const value: WorkspaceContextValue | null =
+ graph && adj && view
+ ? {
+ repo,
+ graph,
+ adj,
+ view,
+ partial,
+ settings,
+ palette,
+ theme,
+ reducedMotion,
+ expandFile,
+ reveal,
+ openView,
+ }
+ : null;
+ const showInspector =
+ ws.inspectorOpen && !!ws.primaryId && viewName !== 'settings' && viewName !== 'docs' && !!value;
+
+ return (
+
+
+ Skip to content
+
+
+
+
+
+
+
+ {repo.name}
+
+
+ {repo.branch ? : null}
+
+
+
+
+
+
+
+
ws.setPaletteOpen(true)}
+ aria-keyshortcuts="Control+K Meta+K"
+ >
+
+ Search
+ Ctrl K
+
+
void reindex()}
+ disabled={indexing}
+ className="btn-quiet"
+ >
+
+
+
setExportOpen(true)}
+ disabled={!ready}
+ className="btn-quiet"
+ >
+
+
+
update({ appearance: { theme: theme === 'dark' ? 'light' : 'dark' } })}
+ className="btn-quiet"
+ >
+ {theme === 'dark' ? : }
+
+ {ws.primaryId ? (
+
ws.setInspector(!ws.inspectorOpen)}
+ aria-pressed={ws.inspectorOpen}
+ className="btn-quiet"
+ >
+
+
+ ) : null}
+
+
+
+
+
+
+ {NAV.map((n) => (
+
+
+ cx(
+ 'block whitespace-nowrap border-l-2 px-3 py-1.5 text-[0.875rem] no-underline transition-colors duration-200 md:px-6 md:py-[7px]',
+ isActive
+ ? 'border-ink font-medium text-ink'
+ : 'border-transparent text-ink-2 hover:text-ink',
+ )
+ }
+ >
+ {n.label}
+
+
+ ))}
+
+
+
+
+ {job?.status === 'running' || (indexing && !value) ? (
+
+ ) : null}
+ {job?.status === 'failed' ? (
+
+ setJobId(null)}>
+ Dismiss
+
+ }
+ >
+ Indexing failed: {job.error}
+
+
+ ) : null}
+ {repo.status === 'failed' && !value && !indexing ? (
+
+ {repo.error ?? 'An unknown error stopped the indexer.'}
+
+ void reindex()}>
+ Index again
+
+
+
+ ) : graphError ? (
+
+ {(graphError as Error).message}
+
+ ) : value ? (
+
+
+
+
+
+ {showInspector ?
: null}
+
+
+
+
+ ) : indexing ? null : (
+
+ )}
+
+
+
+ );
+}
+
+function Shell({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+function Stat({ k, v, tone }: { k: string; v: string; tone?: 'error' | 'warning' }) {
+ return (
+
+
{k}
+
+ {v}
+
+
+ );
+}
+
+export function IndexingProgress({
+ stageIndex,
+ message,
+ processed,
+ total,
+ overlay,
+}: {
+ stageIndex: number;
+ message: string;
+ processed: number;
+ total: number;
+ overlay?: boolean;
+}) {
+ return (
+
+
+
Indexing
+
+ {INDEX_STAGES.map((s, i) => {
+ const state = i < stageIndex ? 'done' : i === stageIndex ? 'active' : 'pending';
+ return (
+
+
+ {state === 'done' ? '✓' : i + 1}
+ {INDEX_STAGE_LABELS[s]}
+
+ {state === 'active' ? (
+
+ {total > 0 ? `${processed.toLocaleString()} / ${total.toLocaleString()}` : ''}
+
+ ) : null}
+
+ );
+ })}
+
+
{message}
+
+
+ );
+}
diff --git a/apps/web/src/workspace/context.tsx b/apps/web/src/workspace/context.tsx
new file mode 100644
index 0000000..abbb1c6
--- /dev/null
+++ b/apps/web/src/workspace/context.tsx
@@ -0,0 +1,35 @@
+import { createContext, useContext } from 'react';
+import type { KnowledgeGraph } from '@vectora/graph-schema';
+import type { RepositoryRecord, Settings } from '@vectora/shared';
+import type { Adjacency, GraphView } from '@/lib/graphView';
+import type { Palette } from '@/lib/tokens';
+
+export interface WorkspaceContextValue {
+ repo: RepositoryRecord;
+ graph: KnowledgeGraph;
+ adj: Adjacency;
+ /** The filtered graph that the 2D and 3D views render. */
+ view: GraphView;
+ partial: boolean;
+ settings: Settings;
+ palette: Palette;
+ theme: 'light' | 'dark';
+ reducedMotion: boolean;
+ /** Shows the symbols of a file, loading them first when the graph was loaded progressively. */
+ expandFile: (fileId: string, force?: boolean) => void;
+ /** Selects a node and makes sure it is visible in the current view. */
+ reveal: (nodeId: string) => void;
+ openView: (
+ view:
+ 'graph' | 'atlas' | 'mindmap' | 'overview' | 'dependencies' | 'search' | 'docs' | 'settings' | 'source',
+ query?: string,
+ ) => void;
+}
+
+export const WorkspaceContext = createContext(null);
+
+export function useWs(): WorkspaceContextValue {
+ const v = useContext(WorkspaceContext);
+ if (!v) throw new Error('useWs must be used inside a workspace');
+ return v;
+}
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
new file mode 100644
index 0000000..2283f7c
--- /dev/null
+++ b/apps/web/vite.config.ts
@@ -0,0 +1,25 @@
+import path from 'node:path';
+import tailwindcss from '@tailwindcss/vite';
+import react from '@vitejs/plugin-react';
+import { defineConfig } from 'vite';
+
+const enginePort = Number(process.env.VECTORA_PORT ?? 4317);
+
+export default defineConfig({
+ root: import.meta.dirname,
+ plugins: [react(), tailwindcss()],
+ resolve: { alias: { '@': path.resolve(import.meta.dirname, 'src') } },
+ server: {
+ host: '127.0.0.1',
+ port: 5173,
+ strictPort: true,
+ proxy: { '/api': { target: `http://127.0.0.1:${enginePort}`, changeOrigin: false } },
+ },
+ worker: { format: 'es' },
+ build: {
+ outDir: path.resolve(import.meta.dirname, '../../dist/web'),
+ emptyOutDir: true,
+ sourcemap: false,
+ chunkSizeWarningLimit: 1200,
+ },
+});
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..cd6086a
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,110 @@
+# Architecture
+
+VECTORA is a TypeScript monorepo (npm workspaces) with two applications and seven packages. Packages are consumed as TypeScript source; the engine is bundled with esbuild for release and the web application with Vite.
+
+```
+browser ──HTTP (loopback)──► local engine ──► repository indexer ──► language adapters
+ │ │ │
+ │ ├──► graph engine (SQLite store, algorithms, exports, explorer)
+ │ └──► search
+ └── graph-layout (Web Worker), search, graph-schema, shared
+```
+
+## Components
+
+### `packages/graph-schema`
+
+The data model: node types, relationship types, extraction methods, source locations, deterministic identifiers, and the functions `validateGraph`, `normalizeGraph` and `computeStats`. Everything else depends on it; it depends only on Zod. See [DATA_MODEL.md](DATA_MODEL.md).
+
+### `packages/language-adapters`
+
+One interface, `LanguageAdapter`, with three implementations:
+
+| Adapter | Languages | Scope | Technique |
+|---|---|---|---|
+| `typescript` | TypeScript, JavaScript | project | One `ts.Program` for all files; the type checker resolves each identifier to its declaration |
+| `python` | Python | file | A helper script using the standard-library `ast` module, run in an isolated interpreter (`python -I`) |
+| `tree-sitter` | Java, Go, Rust, C, C++, C# | file | `web-tree-sitter` with grammar WASM files vendored in `grammars/` |
+
+An adapter returns `FileFacts` per file: symbols, imports, references (either resolved to a definition or carrying only a name), and parse issues. A *project* adapter needs every file of its languages at once; a *file* adapter analyses files independently, which makes its results trivially cacheable.
+
+### `packages/repository-indexer`
+
+The pipeline (`pipeline.ts`) and its stages: `scan.ts` (discovery), `manifests.ts` (package manifests, frameworks, declared entry points), `workers.ts` and `analyze-worker.ts` (bounded worker pool), `resolve.ts` (relationship resolution), `organize.ts` (subsystems, concepts, entry points, execution flows). See [INDEXING_PIPELINE.md](INDEXING_PIPELINE.md).
+
+### `packages/graph-engine`
+
+- `store.ts` — SQLite persistence through `node:sqlite`: repositories, nodes, edges, cached per-file facts, sessions, settings, encrypted credentials.
+- `algorithms.ts` — adjacency index, traversal, shortest path, Tarjan strongly connected components, impact analysis, Louvain communities.
+- `analysis.ts` — the repository analysis record and the overview built from it.
+- `exports.ts` — JSON, GraphML, Markdown analysis, architecture documentation.
+- `explore.ts` — question exploration over the graph.
+
+See [GRAPH_ENGINE.md](GRAPH_ENGINE.md).
+
+### `packages/graph-layout`
+
+Pure layout functions: `layout3d` (hierarchical, force-refined) and `seed2d`. They take plain data so they can run in a Web Worker. See [VISUALIZATION.md](VISUALIZATION.md).
+
+### `packages/search`
+
+`SearchIndex` ranks nodes by exact name, prefix, partial match, qualified name, path and description, and `searchDocuments` searches documentation text. Used by the engine; importable in the browser.
+
+### `packages/shared`
+
+The settings schema (Zod) with defaults, the API contracts shared by engine and web application, index stage names, and secret redaction for logs.
+
+### `apps/local-engine`
+
+A Fastify server bound to `127.0.0.1`.
+
+- `config.ts` — environment configuration; the data directory defaults to `.vectora-data` inside the installation folder.
+- `server.ts` — routes, host and origin checks, security headers, static serving of the built web application.
+- `service.ts` — the `Engine` class: imports, a single-file job queue, graph access, source access, exports, sessions, settings, credentials, data deletion.
+- `security.ts` — ZIP inspection and extraction, path containment, credential vault.
+- `github.ts` — GitHub REST access through Octokit; repositories are downloaded as source archives.
+- `provider.ts` — the optional narrative provider adapter.
+
+### `apps/web`
+
+A React 19 single-page application built with Vite and Tailwind CSS 4.
+
+- `lib/api.ts` — typed fetch wrappers and TanStack Query hooks, including progressive graph loading.
+- `lib/store.ts` — the Zustand workspace store: selection, hover, focus, expansion, filters, saved layouts.
+- `lib/graphView.ts` — `deriveView`, which turns the full graph plus the current filters into what the views render.
+- `lib/tokens.ts` and `styles.css` — design tokens.
+- `workspace/` — the workspace shell and its views; `components/` — inspector, source preview, command palette, export dialog; `pages/` — landing, import, About.
+
+## How the three views stay in step
+
+1. `useGraph` loads the knowledge graph once per repository into the TanStack Query cache.
+2. `WorkspaceLayout` builds one adjacency index and calls `deriveView` with the settings and the workspace store. The result is provided through `WorkspaceContext`.
+3. The Graph Explorer and the 3D Atlas render `view.nodes` and `view.edges`. The Mind Map builds its hierarchy from the same adjacency index.
+4. Selection, hover, focus and expansion live in the workspace store. Any view that changes them changes them for all views, and the inspector reads the same store.
+5. `revealTick` in the store tells whichever view is mounted to bring the primary selection into view.
+
+## Request flow for an import
+
+1. The browser posts to `/api/import/{local|zip|github}` and receives a job.
+2. The engine queues the job. Jobs run one at a time so that indexing never competes with itself for memory.
+3. The job acquires the source (reads a folder in place, extracts an archive, or downloads one from GitHub), then calls `indexRepository`.
+4. The result — graph, analysis, per-file facts — is written to SQLite in one transaction per table.
+5. The browser polls `/api/jobs/:id` and opens the workspace when the job completes.
+
+## Build and release layout
+
+`npm run build` produces:
+
+```
+dist/web/ static web application
+dist/engine/server.mjs engine bundle
+dist/engine/analyze-worker.mjs
+dist/engine/grammars/ Tree-sitter grammars
+dist/engine/python/ Python helper
+```
+
+`typescript` and `web-tree-sitter` stay external to the bundle because they load files from their own package folders at run time, so `node_modules` must be present next to `dist`.
+
+## Technology decisions
+
+The reasoning behind the main choices (SQLite engine, Sigma version, archive-based GitHub import, vendored grammars, package layout) is recorded in [../DECISIONS.md](../DECISIONS.md).
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..ad17196
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,25 @@
+# Changelog
+
+All notable changes to VECTORA are recorded here. Versions follow semantic versioning.
+
+## 0.1.0 — 2026-11-12
+
+First version.
+
+### Added
+
+- Repository import from GitHub (public, and private with an access token), ZIP archives and local folders, plus a bundled sample repository.
+- Indexing pipeline: discovery with ignore rules, syntax analysis, relationship resolution, execution flows, subsystem and concept organisation, graph validation.
+- Language adapters: TypeScript and JavaScript (TypeScript Compiler API with type-checked resolution), Python (`ast`), Java, Go, Rust, C, C++ and C# (Tree-sitter).
+- Knowledge graph with 18 node types and 15 relationship types, each carrying evidence, extraction method and confidence. Unresolved references are reported, not drawn.
+- Incremental indexing with a per-file cache and an unchanged-repository fast path; analysis in worker threads.
+- SQLite persistence using the engine built into Node.js.
+- Graph Explorer (2D), 3D Atlas and Mind Map with shared selection, focus mode, filters and saved layouts.
+- Inspector with source preview and evidence for each relationship.
+- Command palette, search page, documentation search and question exploration with file and line citations.
+- Dependency Explorer: packages, subsystem dependencies, circular imports, execution flows, unresolved references.
+- Exports: JSON, GraphML, Markdown analysis, architecture document, SVG and PNG.
+- Settings for appearance, graph, performance, repository and privacy; light and dark themes; reduced-motion support.
+- Local engine bound to the loopback interface with host and origin checks, safe archive handling and encrypted credential storage.
+- Unit, integration and browser tests; performance benchmarks; documentation.
+- README with screenshots and a recorded tour captured from the application; continuous integration workflow.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..2d049bf
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,61 @@
+# Contributing
+
+## Setup
+
+Requirements: Node.js 22.13 or newer, npm, and Python 3.8 or newer if you work on Python analysis.
+
+```bash
+npm install
+```
+
+```bash
+npm run dev
+```
+
+`npm run dev` starts the engine on port 4317 (restarting on changes) and the Vite dev server on port 5173, which proxies `/api` to the engine. Open .
+
+Data is written to `.vectora-data` in the repository folder; test and benchmark scratch files go to `.bench-tmp`. Both are ignored by git.
+
+## Commands
+
+| Command | Purpose |
+|---|---|
+| `npm run dev` | Engine and web application with reloading |
+| `npm run build` | Production build into `dist/` |
+| `npm start` | Run the production build |
+| `npm run typecheck` | TypeScript, strict mode, whole repository |
+| `npm run lint` | ESLint |
+| `npm run format` | Prettier |
+| `npm test` | Unit and integration tests |
+| `npm run test:coverage` | The same with coverage |
+| `npm run test:e2e` | Browser journeys against the production build |
+| `npm run bench` | Performance benchmarks |
+| `npm run licenses` | Regenerate `docs/LICENSES.md` |
+| `npx tsx scripts/index-cli.ts --edges --unresolved` | Index a folder from the command line and print the result |
+
+## Layout
+
+See [ARCHITECTURE.md](ARCHITECTURE.md). Packages export TypeScript source (`"exports": "./src/index.ts"`); there is no per-package build step.
+
+## Guidelines
+
+- **Evidence.** A relationship needs a source location and evidence text. Do not add resolution that guesses: if scope evidence is missing, report the reference as unresolved. New heuristics must set `verified: false` and an honest confidence.
+- **Facts and interpretation stay apart.** Derived information goes into separate nodes, edges or metadata with its own extraction method.
+- **Tests come with the change.** Add a fixture to `samples/sample-repo` or a temporary tree in the test, and assert both what is found and what must not be found.
+- **No execution of repository content.** No scripts, no hooks, no dependency installation, no importing analysed modules.
+- **Privacy.** No network request without a user action. Never log request bodies or credentials. New exports must respect the source-inclusion setting.
+- **Interface.** Follow [DESIGN_SYSTEM.md](DESIGN_SYSTEM.md): tokens only, sentence case, no decoration, visible focus, accessible names, colour never the only signal. User-facing text describes the product as a code intelligence and repository analysis platform.
+- **Dependencies.** Justify each new one, check its licence, run `npm run licenses` and `npm audit --omit=dev`.
+- **Before a pull request:** `npm run typecheck`, `npm run lint`, `npm test`, and for interface changes `npm run build` followed by `npm run test:e2e`.
+
+## Adding a language
+
+See “Adding a language” in [INDEXING_PIPELINE.md](INDEXING_PIPELINE.md).
+
+## Commit messages
+
+Imperative mood, a short subject line, and a body that says what changed and why.
+
+## Project records
+
+Update [../IMPLEMENTATION_CHECKLIST.md](../IMPLEMENTATION_CHECKLIST.md), [../DECISIONS.md](../DECISIONS.md) and [../KNOWN_ISSUES.md](../KNOWN_ISSUES.md) when your change affects them, and add an entry to [CHANGELOG.md](CHANGELOG.md).
diff --git a/docs/DATA_MODEL.md b/docs/DATA_MODEL.md
new file mode 100644
index 0000000..4066da2
--- /dev/null
+++ b/docs/DATA_MODEL.md
@@ -0,0 +1,116 @@
+# Data model
+
+The knowledge graph is defined in `packages/graph-schema/src/index.ts`. Schema version: 1.
+
+## Node
+
+| Field | Type | Meaning |
+|---|---|---|
+| `id` | string | Deterministic identifier, unique in the repository |
+| `name` | string | Display name |
+| `type` | NodeType | One of the types below |
+| `repositoryId` | string | Owning repository |
+| `filePath` | string or null | Repository-relative path, POSIX separators |
+| `location` | SourceLocation or null | `path`, `startLine`, `endLine`, optional columns |
+| `description` | string, optional | Doc comment or manifest description |
+| `language` | string or null | Language id |
+| `metadata` | object | Type-specific details (see below) |
+| `extractionMethod` | ExtractionMethod | How the node was found |
+| `confidence` | number 0–1 | Extractor confidence |
+| `verified` | boolean | `true` for structural facts, `false` for derived interpretations |
+
+### Node types
+
+| Type | What it represents | Identifier |
+|---|---|---|
+| Repository | The repository root | `repo:root` |
+| Directory | A folder containing indexed files | `dir:` |
+| Module | A folder that is a package (has `package.json`, `__init__.py`, `mod.rs`, `index.*`, `go.mod`, `Cargo.toml`) | `dir:` |
+| File | A source or text file | `file:` |
+| Configuration | A file recognised as configuration (manifests, tool configs, CI files) | `file:` |
+| Package | An external package, declared in a manifest or imported | `pkg:` |
+| Class | Class, struct, record | `sym:#` |
+| Interface | Interface, trait | `sym:…` |
+| Type | Type alias, enum | `sym:…` |
+| Function | Function | `sym:…` |
+| Method | Method, constructor, accessor | `sym:…` |
+| Variable | Mutable module-level binding | `sym:…` |
+| Constant | Constant, macro constant | `sym:…` |
+| Component | UI component (a capitalised function returning JSX, or a class extending a component base) | `sym:…` |
+| ApiEndpoint | A registered route | `sym:#route: ` |
+| DatabaseModel | An ORM model or table definition | `sym:…` |
+| Subsystem | A group of files derived from the repository layout | `sub:` |
+| Concept | A group of files derived from identifier and path evidence | `concept:` |
+
+Qualified names join parent and child with a dot (`NoteService.create`). A second declaration with the same qualified name in one file gets the suffix `~2`.
+
+### Common metadata
+
+- Files: `size`, `lines`, `hash`, `analyzed`, `adapter`, `parseErrors`, `subsystem`, `concepts`, `entryPoint`, `builtinImports`, `test`.
+- Symbols: `qualifiedName`, `exported`, `signature`, `decorators`, `async`, `subsystem`.
+- Endpoints: `httpMethod`, `routePath`, `framework`, `flowSteps`.
+- Packages: `declared`, `version`, `ecosystem`, `dev`.
+- Every node: `community` (Louvain community number).
+
+## Relationship (edge)
+
+| Field | Type | Meaning |
+|---|---|---|
+| `id` | string | `||` |
+| `source`, `target` | string | Node ids |
+| `type` | EdgeType | One of the types below |
+| `evidence` | string | The source line or statement that justifies the relationship |
+| `location` | SourceLocation or null | Where the evidence is |
+| `extractionMethod` | ExtractionMethod | How it was found |
+| `confidence` | number 0–1 | |
+| `verified` | boolean | |
+| `metadata` | object, optional | e.g. `occurrences`, `lines`, `operation`, `count`, `kinds`, `crossSubsystem` |
+
+The same relationship found several times is one edge; `metadata.occurrences` counts them and `metadata.lines` lists up to 20 line numbers. The first evidence is kept.
+
+### Relationship types
+
+| Type | From → to | Produced by |
+|---|---|---|
+| CONTAINS | repository → directory/subsystem/concept; directory → file; file → symbol; class → method | file system, grouping |
+| DEFINED_IN | nested symbol → its file | file system |
+| EXPORTS | file → exported top-level symbol; file → re-exported symbol | adapters |
+| IMPORTS | file → file, file → directory (Go package), file → package | adapters and import resolution |
+| CALLS | symbol → function or method | type checker, scope resolution, name resolution |
+| INSTANTIATES | symbol → class | same |
+| EXTENDS | class → class, interface → interface, struct → embedded struct | same |
+| IMPLEMENTS | class → interface, type → trait | same |
+| USES | symbol → symbol it references without calling (types, values, rendered components) | same |
+| ROUTES_TO | endpoint → handler | framework patterns |
+| READS_FROM, WRITES_TO | symbol → database model | call patterns on known models; always `verified: false` |
+| DEPENDS_ON | manifest → declared package; subsystem → subsystem (aggregated) | manifest, aggregation |
+| BELONGS_TO | file → subsystem; file → concept | structural grouping, keyword evidence |
+| RELATED_TO | concept ↔ concept | aggregation; `verified: false` |
+
+### Extraction methods
+
+| Method | Meaning | Typical confidence |
+|---|---|---|
+| `filesystem` | Directory and file structure | 1 |
+| `manifest` | Package manifest | 1 |
+| `typescript-compiler` | TypeScript syntax (declarations, import statements) | 1 |
+| `typescript-checker` | Identifier resolved to its declaration by the type checker | 1 |
+| `python-ast`, `tree-sitter` | Syntax tree | 1 for declarations |
+| `scope-resolution` | Name resolved through an import or same-file scope | 0.9–0.95 |
+| `name-resolution` | Unique name among files visible by language rules (same package, included header) | 0.7, unverified |
+| `pattern` | Framework or data-access pattern | 0.7–0.95 |
+| `structural-grouping` | Subsystem from layout | 0.9 |
+| `keyword-evidence` | Concept from names and paths | 0.4–0.9, unverified |
+| `aggregation` | Derived from other edges | varies |
+
+## Unresolved references
+
+A reference that cannot be tied to a definition never becomes an edge. It is recorded as `{ fromPath, fromSymbol, kind, name, line, reason }`, counted in `stats.unresolvedCount`, grouped by reason, and up to 500 samples are stored with the analysis.
+
+## Integrity rules
+
+`validateGraph` checks: unique node ids, unique edge ids, no edge whose endpoints are missing, evidence present on every edge, schema conformance, and reports self-loops (other than recursion) and orphan nodes as warnings. `normalizeGraph` runs before validation: the first node with an id wins, duplicate edges merge, edges with a missing endpoint are dropped, and paths are normalised. The numbers of removed, merged and dropped items are stored with the analysis.
+
+## Persistence
+
+SQLite tables: `repositories`, `nodes (repo_id, id, type, name, file_path, data)`, `edges (repo_id, id, source, target, type, data)`, `file_facts (repo_id, path, hash, facts)`, `sessions`, `settings`, `credentials`, `meta`. The `data` column holds the full JSON of the node or edge; the other columns exist for filtering.
diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md
new file mode 100644
index 0000000..bd706a5
--- /dev/null
+++ b/docs/DESIGN_SYSTEM.md
@@ -0,0 +1,125 @@
+# Design system
+
+VECTORA's interface follows Japanese minimalist principles: **kanso** (remove what is not needed), **ma** (space with intent), **shibui** (understated refinement) and a little **wabi-sabi** (material character without clutter). It should read like an editorial publication that happens to be a developer tool.
+
+Tokens live in `apps/web/src/lib/tokens.ts` (used by the WebGL renderers) and are mirrored as CSS custom properties in `apps/web/src/styles.css`. A unit test keeps the two in step.
+
+## Colour
+
+### Light
+
+| Token | Value | Use |
+|---|---|---|
+| Canvas | `#F8F7F2` | Page background |
+| Washi (surface) | `#F3F1EA` | Panels, bands |
+| Elevated | `#FBFAF6` | Inputs, dialogs |
+| Sumi (text) | `#20211F` | Primary text |
+| Secondary text | `#5F5D55` | Supporting text |
+| Stone (muted) | `#8C897F` | Metadata |
+| Ash | `#B8B3A7` | Quiet borders, external nodes |
+| Line | `#DCD8CE` | Rules and borders |
+| Indigo | `#34445C` | Accent, selection, active relationships |
+| Clay | `#A18C72` | Second accent, derived relationships |
+
+### Dark
+
+| Token | Value |
+|---|---|
+| Canvas | `#171816` |
+| Surface | `#20211F` |
+| Elevated | `#272824` |
+| Primary text | `#ECEAE2` |
+| Secondary text | `#A7A499` |
+| Muted text | `#77766D` |
+| Border | `#373832` |
+| Indigo accent | `#8292AD` |
+| Clay accent | `#B39A7A` |
+
+### Semantic
+
+| Role | Light | Dark |
+|---|---|---|
+| Success | `#55704F` | `#8FAE8A` |
+| Warning | `#7D5C1B` | `#C9A566` |
+| Error | `#964436` | `#D08C7C` |
+| Selection | Indigo | Indigo accent |
+| Active relationship | Indigo | Indigo accent |
+| Unverified relationship | Clay | Clay accent |
+
+Contrast is tested: primary text reaches at least 7:1 on every surface, secondary text and all semantic colours at least 4.5:1, muted text at least 3:1 (it is used only for metadata).
+
+Colour is never the only signal. Node types have glyphs, sizes and 3D shapes; derived relationships are dashed or thin and labelled; status messages begin with a word (“Error.”, “Note.”).
+
+## Typography
+
+| Role | Family | Use |
+|---|---|---|
+| Primary | Zen Kaku Gothic New (400, 500, 700) | Interface text |
+| Editorial | Shippori Mincho (400, 600) | Page and section headings, the About text, the brand line |
+| Technical | IBM Plex Mono (400, 500) | Paths, symbol names, code, counts, metadata |
+
+The fonts are bundled (Latin subsets) through `@fontsource`; nothing is fetched from a font service.
+
+| Level | Size | Family |
+|---|---|---|
+| Display | 44–64 px, fluid | Mincho |
+| Page heading | 30–40 px, fluid | Mincho |
+| Section heading | 20–24 px | Mincho |
+| Body | 15 px, line-height 1.6 | Gothic |
+| Label | 13 px | Gothic |
+| Metadata | 12 px | Mono |
+
+Rules: sentence case everywhere; no all-caps labels; body lines kept under about 68 characters; the only letter-spaced text is the wordmark. Not every heading is large — inside the workspace, headings are 18–24 px.
+
+## Space and shape
+
+- A 4 px base. Controls use 4–8 px gaps, components 12–20 px, sections 24–40 px, editorial pages 48–96 px.
+- Layouts sit on a 12-column grid and are deliberately asymmetrical: headings start in the second column, body text in a narrower column further right.
+- Borders are 1 px `Line`. Shadows are used only under dialogs and popovers.
+- Corners are square or 2–3 px. There are no pill shapes.
+- Surfaces are separated by rules and tone, not by cards. Lists are rows with a bottom rule.
+
+## Signature element
+
+The brand is set vertically along the left edge of the landing page (`writing-mode: vertical-rl`), a nod to Japanese vertical typesetting. It is the one decorative gesture; everything else stays quiet.
+
+## Motion
+
+| Use | Duration |
+|---|---|
+| Interface transitions (hover, focus, rows) | 200 ms |
+| Panels and dialogs | 380 ms |
+| Camera transitions | 600–820 ms, eased in and out |
+| Layout settling | about 2.4 s (subtle) or 4.2 s (standard) |
+
+No bounce, no pulsing, no looping decoration. The only ambient motion is the slow rotation of the landing illustration, which stops when it is off screen. With reduced motion (system setting or Settings) transitions are instant, camera moves jump, layouts are computed in one step and the illustration is still.
+
+## Components
+
+Defined as CSS classes in `styles.css` and small React wrappers in `components/ui.tsx`.
+
+| Component | Notes |
+|---|---|
+| Button (`.btn`, `-primary`, `-quiet`, `-sm`, `-icon`) | 36 px high (28 px small). Primary is Sumi on Canvas and turns Indigo on hover. Pressed state uses `aria-pressed` |
+| Field (`.field`) | Inputs, selects and text areas; focus is an Indigo border |
+| Toggle, SelectField, NumberField | Label on the left with an optional hint, control on the right |
+| Dialog | Radix dialog with overlay; title in Mincho |
+| Tag (`.tag`, `.tag-derived`) | Small bordered label; the derived variant is dashed |
+| Notice | Left rule in the semantic colour, starts with a status word |
+| Link row (`.link-row`) | List row with a bottom rule; shifts 6 px on hover |
+| Data table (`.data-table`) | Rules only, numeric columns right-aligned in mono |
+| TypeMark | Glyph plus type name |
+| Section heading | Mincho heading over a rule, optional note on the right |
+
+## Accessibility
+
+- Every page has one `h1`, one `main`, a skip link, and landmarks with names.
+- All controls have accessible names; form fields have labels.
+- Focus is always visible (2 px Indigo outline).
+- Dialogs and the command palette trap focus and close with Escape.
+- The graph canvas is keyboard operable; the Mind Map and Search offer the same content as focusable elements.
+- These points are checked by the browser tests.
+
+## Copy
+
+Plain verbs, sentence case, no exclamation marks. Buttons say what happens (“Index this folder”, “Download and index”). Errors say what went wrong and what to do. Empty states say what will appear and how to make it appear. The product is described as a code intelligence, repository analysis and knowledge exploration platform.
diff --git a/docs/GRAPH_ENGINE.md b/docs/GRAPH_ENGINE.md
new file mode 100644
index 0000000..6904fac
--- /dev/null
+++ b/docs/GRAPH_ENGINE.md
@@ -0,0 +1,69 @@
+# Graph engine
+
+`packages/graph-engine` holds persistence, graph algorithms, exports and question exploration.
+
+## Persistence (`store.ts`)
+
+VECTORA uses the SQLite engine that ships inside Node.js (`node:sqlite`). No native module is compiled or downloaded. The database is one file, `vectora.db`, in the data directory, opened in WAL mode.
+
+| Table | Contents |
+|---|---|
+| `repositories` | One row per repository: name, source kind, origin, branch, root path, status, counts, error, analysis JSON |
+| `nodes` | `repo_id, id, type, name, file_path, data` — `data` is the full node as JSON |
+| `edges` | `repo_id, id, source, target, type, data` |
+| `file_facts` | Cached adapter output per file with its content hash |
+| `sessions` | One saved workspace session per repository |
+| `settings` | A single validated settings document |
+| `credentials` | Encrypted values (see [SECURITY.md](SECURITY.md)) |
+| `meta` | Database version |
+
+Indexes: `nodes(repo_id, type)`, `nodes(repo_id, file_path)`, `edges(repo_id, source)`, `edges(repo_id, target)`.
+
+Saving a graph replaces the repository's rows inside one transaction, using multi-row inserts. Loading returns the whole graph, or only chosen node types with the edges between them (`loadGraph(id, { types })`), or the symbols of one file with every edge touching them (`loadNodesByFile`).
+
+The engine keeps at most two loaded repositories in memory, each with its adjacency index and search index.
+
+### Migrations
+
+`meta.db_version` records the schema version (currently 2). Version 1 used `WITHOUT ROWID` tables for graph rows; on opening such a database those tables are rebuilt and the affected repositories are marked for re-indexing.
+
+## Algorithms (`algorithms.ts`)
+
+| Function | Purpose |
+|---|---|
+| `buildIndex` | Maps of nodes, edges, outgoing and incoming edges |
+| `traverse` | Breadth-first search over chosen relationship types in either direction, bounded by depth and node count |
+| `shortestPath` | Shortest directed path between two nodes |
+| `findCycles` | Strongly connected components (iterative Tarjan); used for circular imports |
+| `cyclePath` | One concrete cycle inside a component |
+| `impactOf` | Reverse reachability from a node and its members: what depends on it |
+| `containerChain` | The chain of containers above a node |
+| `assignCommunities` | Louvain communities on the undirected dependency projection, fixed seed |
+
+## Overview (`analysis.ts`)
+
+`buildOverview` derives the workspace overview from the graph and the analysis record: languages, frameworks, entry points, subsystems with inbound and outbound counts, concepts with their strongest evidence, largest modules, dependency statistics, and an architecture summary. Each summary statement carries an evidence string and a `verified` flag.
+
+## Exports (`exports.ts`)
+
+| Format | Function | Notes |
+|---|---|---|
+| JSON | `exportJson` | All nodes and edges with ids, locations, methods and confidence |
+| GraphML | `exportGraphML` | For Gephi, yEd, Cytoscape; attributes declared as keys |
+| Markdown analysis | `exportMarkdownAnalysis` | Index, languages, summary, subsystems, dependencies, unresolved |
+| Architecture document | `exportArchitectureDoc` | Subsystem dependencies with a Mermaid diagram, endpoints, models, flows, cycles |
+
+By default `stripSource` removes source text before a graph export: edge evidence becomes `path:line`, signatures and doc comments are dropped. Source text is included only when requested explicitly. Node ids and relationships are identical either way.
+
+## Question exploration (`explore.ts`)
+
+`explore(question, index, analysis, find)` answers from the graph only.
+
+1. **Intent** is detected by pattern: callers, dependents, impact, cycles, startup, data access, trace, explain.
+2. **Subject** is taken from quoted or code-formatted text, otherwise from the words that look like identifiers.
+3. The subject is located with the search index.
+4. The relevant relationships are retrieved with the algorithms above.
+5. The answer is a list of statements. Each has `verified` and a list of evidence references (`path`, `startLine`, `endLine`, snippet).
+6. Anything the graph cannot establish goes into `caveats`. When the subject is not found, the answer has no statements at all.
+
+The engine can optionally pass the finished statements to an external narrative provider (`apps/local-engine/src/provider.ts`) to be rewritten as prose. That is off by default; the statements and evidence shown to the user always come from the graph.
diff --git a/docs/INDEXING_PIPELINE.md b/docs/INDEXING_PIPELINE.md
new file mode 100644
index 0000000..fbffee9
--- /dev/null
+++ b/docs/INDEXING_PIPELINE.md
@@ -0,0 +1,94 @@
+# Indexing pipeline
+
+`indexRepository` in `packages/repository-indexer/src/pipeline.ts` runs the stages below and reports progress per stage. The engine shows these stages on the import screen: acquire, discover, analyse, resolve, flows, organise, graph, persist.
+
+Nothing in the analysed repository is ever executed, and its dependencies are never installed.
+
+## Stage 1 — Discovery (`scan.ts`, `manifests.ts`)
+
+1. All files are listed with `fast-glob`. `node_modules` and version-control folders are never entered. Symbolic links are not followed and are skipped.
+2. Ignore rules are applied: the patterns from Settings and every `.gitignore` in the tree, each relative to its own folder.
+3. Files are skipped, and counted by reason, when they have a binary extension, contain a NUL byte in the first 8 KB, exceed the size limit (512 KB by default), or look generated or minified.
+4. Each remaining file is read once to compute a SHA-1 content hash and a line count. The language is detected from the extension or file name.
+5. If the repository exceeds the file limit, analysable source files are kept in preference to others and the result is marked as truncated.
+6. Manifests are parsed as data: `package.json`, `requirements*.txt`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `pom.xml`, `*.csproj`. They yield declared dependencies (with manifest line), workspace packages, frameworks, declared entry points and a description. Without a manifest description the first paragraph of the root README is used.
+
+## Stage 2 — Syntax analysis (`workers.ts`, language adapters)
+
+Files are grouped by adapter.
+
+- **File-scope adapters** (Python, Tree-sitter): a file whose hash matches the cache reuses its cached facts. The rest are batched (150 files) and analysed in worker threads; Python batches run in a separate, isolated interpreter process.
+- **Project-scope adapter** (TypeScript/JavaScript): one task builds a `ts.Program` over all TypeScript and JavaScript files. Module resolution is done by VECTORA against the list of indexed files only (relative paths, `tsconfig` path aliases, workspace packages), so the compiler never reads outside the repository except for its own library definitions.
+
+Workers are bounded by the *Indexing workers* setting. If a worker cannot start, analysis falls back to the main process. A file that fails to parse is reported under parse issues and remains in the graph as a file without symbols.
+
+### What is extracted
+
+Classes, functions, methods, interfaces, types, enums, variables, constants, components, database models, API endpoints; imports and re-exports; calls, instantiations, inheritance, interface implementation, type and value uses; decorators, annotations and attributes; doc comments and signatures.
+
+Framework patterns recognised:
+
+| Kind | Patterns |
+|---|---|
+| API endpoints | `app.get('/x', …)` style router calls, `@Get()`/`@Controller()` decorators, Next.js `app/**/route.ts` exports, Flask/FastAPI decorators, Django `urlpatterns`, Spring `@GetMapping` etc., ASP.NET `[HttpGet]`, Go `HandleFunc`/router methods, Rust `#[get("/x")]` |
+| Database models | Classes extending model bases or carrying entity decorators, Drizzle table factories, Mongoose/Sequelize definitions, SQLAlchemy/Django models, `@Entity`, GORM struct tags, Diesel/SeaORM derives, `[Table]`/`DbContext` |
+| Components | Capitalised functions that return JSX, wrapped components (`memo`, `forwardRef`), classes extending `Component` |
+
+## Stage 3 — Relationship resolution (`resolve.ts`)
+
+Facts become nodes and edges.
+
+**TypeScript and JavaScript.** The adapter has already resolved references with the type checker. An identifier becomes an edge only when the checker ties it to a declaration inside the repository. Imports of external packages become edges to Package nodes. A call the checker cannot tie to anything is recorded as unresolved.
+
+**Other languages.** The resolver builds, per file, a table of import bindings and a list of files visible without qualification, using each language's own rules:
+
+| Language | Import resolution | Visible without import |
+|---|---|---|
+| Python | Relative imports exactly; absolute imports by dotted-path suffix, nearest match | `from x import *` targets |
+| Java | Package declaration plus class name | Same package, wildcard imports |
+| Go | `go.mod` module path plus directory | Same directory and package |
+| Rust | `crate::`, `self::`, `super::` paths to `x.rs` or `x/mod.rs` | Glob imports |
+| C, C++ | `#include "x"` relative, else a unique path suffix | Included files |
+| C# | `using` namespace to files declaring it | Same namespace |
+
+A name is then resolved in this order: a symbol in the same file (`scope-resolution`, 0.95); an import binding that leads to a file containing the symbol (`scope-resolution`, 0.9); a unique symbol among the visible files (`name-resolution`, 0.7, marked unverified). For languages with an implicit receiver, an unqualified call may resolve to a member of the enclosing type. `self.x()` / `this.x()` resolve to a member of the enclosing class if it exists.
+
+**What is never done:** a name is not resolved merely because a symbol with that name exists somewhere in the repository. If several visible definitions match, the reference is ambiguous and stays unresolved. Calls on receivers whose type is unknown stay unresolved. Calls into imported external packages are known to be external and are not counted.
+
+**Data access.** A call on, or passing, a symbol that resolved to a database model, with a method name that reads or writes, produces `READS_FROM` or `WRITES_TO`. These are always marked unverified.
+
+## Stage 4 — Execution flows (`organize.ts`)
+
+- **Entry points:** files named by manifests (`main`, `bin`, start scripts), files defining a `main` function, and conventional entry files that nothing imports.
+- **Flows:** from each API endpoint and each entry point, route and call relationships are followed breadth-first (up to 8 steps) to the first data access, or otherwise to the deepest reachable symbol. Flows are stored with the analysis and marked as derived.
+- Relationships that cross a subsystem boundary are flagged.
+
+## Stage 5 — Semantic organisation (`organize.ts`)
+
+- **Subsystems** come from layout: workspace packages when there are at least two, otherwise the folders below the source root (wrapper folders such as `src/` that hold nearly everything are descended into). Every file belongs to exactly one subsystem. Dependencies between subsystems are aggregated into `DEPENDS_ON` edges whose evidence names the count and one concrete example.
+- **Concepts** come from evidence in path segments, symbol names and imported package names, matched against a fixed vocabulary (authentication, data storage, API layer, and so on), plus structural evidence such as “defines an endpoint”. A file joins a concept when its evidence score reaches a threshold; a concept exists when at least two files join it. Concept nodes and memberships are marked unverified and carry their evidence text.
+- **Communities** are computed with the Louvain method on the dependency graph with a fixed seed, and stored as `metadata.community`.
+
+Interpretations are never mixed into facts: subsystem, concept and community information is additional nodes, edges and metadata, each labelled with its extraction method.
+
+## Stage 6 — Graph generation
+
+`normalizeGraph` removes duplicate nodes, merges duplicate edges and drops edges with a missing endpoint. `validateGraph` checks integrity. Statistics and language coverage are computed. The analysis record stores everything a user may want to audit: skipped files by reason, parse issues, unresolved references by reason with samples, validation and normalisation results.
+
+## Stage 7 — Persistence
+
+The engine writes the graph, the analysis and the per-file facts to SQLite. See [GRAPH_ENGINE.md](GRAPH_ENGINE.md).
+
+## Incremental indexing
+
+On re-index, the cached facts of the previous run are passed in.
+
+- File-scope adapters re-analyse only files whose hash changed.
+- For TypeScript and JavaScript, the files to re-analyse are: changed files; files whose cached imports or resolved references point into a changed or deleted file; and, when files were added, files with unresolved imports or references (a new file may now satisfy them). If that set is empty the compiler is not started at all. Otherwise a program is still created over all files, because the checker needs every declaration, but only the files in the set are walked.
+- Resolution, organisation and graph generation always run over the full set of facts, so the graph never contains relationships left over from an earlier state of a file.
+
+The cache can be switched off or cleared in Settings, and a full re-index can be requested through the API (`{ "full": true }`).
+
+## Adding a language
+
+Implement `LanguageAdapter` (`id`, `technique`, `languages`, `scope`, `analyze`) and register it in `createAdapters`. If the adapter emits unresolved names, add the language's import and visibility rules to `resolve.ts`. Add fixture files to the sample repository and assertions to `tests/unit/indexing.test.ts`.
diff --git a/docs/LICENSES.md b/docs/LICENSES.md
new file mode 100644
index 0000000..3a8d4a0
--- /dev/null
+++ b/docs/LICENSES.md
@@ -0,0 +1,473 @@
+# Dependency licence inventory
+
+Generated by `npm run licenses` on 2026-11-12 from the installed packages. It covers the production dependency closure of every workspace (434 packages); development tools are not included.
+
+## Summary
+
+| Licence | Packages |
+|---|---:|
+| MIT | 389 |
+| ISC | 21 |
+| Apache-2.0 | 7 |
+| BlueOak-1.0.0 | 6 |
+| BSD-3-Clause | 6 |
+| OFL-1.1 | 3 |
+| 0BSD | 1 |
+| UNKNOWN | 1 |
+
+## Needs review
+
+These packages declare a licence outside the permissive set used for the automatic check:
+
+- webgl-constants@1.1.1: UNKNOWN
+
+## Bundled assets that are not npm packages
+
+| Asset | Version | Licence |
+|---|---|---|
+| tree-sitter-java grammar (WASM) | 0.23.5 | MIT |
+| tree-sitter-go grammar (WASM) | 0.25.0 | MIT |
+| tree-sitter-rust grammar (WASM) | 0.24.0 | MIT |
+| tree-sitter-c grammar (WASM) | 0.24.1 | MIT |
+| tree-sitter-cpp grammar (WASM) | 0.23.4 | MIT |
+| tree-sitter-c-sharp grammar (WASM) | 0.23.5 | MIT |
+| Zen Kaku Gothic New, Shippori Mincho, IBM Plex Mono (via @fontsource) | — | SIL OFL 1.1 |
+
+## Packages
+
+| Package | Version | Licence |
+|---|---|---|
+| @babel/runtime | 7.29.14 | MIT |
+| @codemirror/autocomplete | 6.20.4 | MIT |
+| @codemirror/commands | 6.11.1 | MIT |
+| @codemirror/lang-cpp | 6.0.3 | MIT |
+| @codemirror/lang-css | 6.3.1 | MIT |
+| @codemirror/lang-go | 6.0.1 | MIT |
+| @codemirror/lang-html | 6.4.12 | MIT |
+| @codemirror/lang-java | 6.0.2 | MIT |
+| @codemirror/lang-javascript | 6.2.5 | MIT |
+| @codemirror/lang-json | 6.0.2 | MIT |
+| @codemirror/lang-markdown | 6.5.2 | MIT |
+| @codemirror/lang-python | 6.2.1 | MIT |
+| @codemirror/lang-rust | 6.0.2 | MIT |
+| @codemirror/language | 6.13.1 | MIT |
+| @codemirror/lint | 6.9.7 | MIT |
+| @codemirror/search | 6.7.2 | MIT |
+| @codemirror/state | 6.7.6 | MIT |
+| @codemirror/streamparser | 6.0.0 | MIT |
+| @codemirror/view | 6.43.15 | MIT |
+| @dagrejs/dagre | 3.1.3 | MIT |
+| @dagrejs/graphlib | 4.0.6 | MIT |
+| @dimforge/rapier3d-compat | 0.12.0 | Apache-2.0 |
+| @fastify/accept-negotiator | 2.1.2 | MIT |
+| @fastify/ajv-compiler | 4.0.7 | MIT |
+| @fastify/busboy | 3.2.5 | MIT |
+| @fastify/cors | 11.4.1 | MIT |
+| @fastify/deepmerge | 3.2.4 | MIT |
+| @fastify/error | 4.2.1 | MIT |
+| @fastify/fast-json-stringify-compiler | 5.1.1 | MIT |
+| @fastify/forwarded | 3.1.2 | MIT |
+| @fastify/merge-json-schemas | 0.2.2 | MIT |
+| @fastify/multipart | 10.1.8 | MIT |
+| @fastify/proxy-addr | 5.1.2 | MIT |
+| @fastify/send | 4.2.1 | MIT |
+| @fastify/static | 10.1.10 | MIT |
+| @floating-ui/core | 1.8.0 | MIT |
+| @floating-ui/dom | 1.8.0 | MIT |
+| @floating-ui/react-dom | 2.1.9 | MIT |
+| @floating-ui/utils | 0.2.12 | MIT |
+| @fontsource/ibm-plex-mono | 5.3.0 | OFL-1.1 |
+| @fontsource/shippori-mincho | 5.3.0 | OFL-1.1 |
+| @fontsource/zen-kaku-gothic-new | 5.3.1 | OFL-1.1 |
+| @isaacs/nonrecursive-glob-match | 1.1.0 | BlueOak-1.0.0 |
+| @lezer/common | 1.5.5 | MIT |
+| @lezer/cpp | 1.1.6 | MIT |
+| @lezer/css | 1.3.9 | MIT |
+| @lezer/go | 1.0.1 | MIT |
+| @lezer/highlight | 1.2.5 | MIT |
+| @lezer/html | 1.3.13 | MIT |
+| @lezer/java | 1.1.5 | MIT |
+| @lezer/javascript | 1.5.6 | MIT |
+| @lezer/json | 1.0.3 | MIT |
+| @lezer/lr | 1.4.12 | MIT |
+| @lezer/markdown | 1.8.1 | MIT |
+| @lezer/python | 1.1.19 | MIT |
+| @lezer/rust | 1.0.3 | MIT |
+| @lukeed/ms | 2.0.2 | MIT |
+| @marijn/find-cluster-break | 1.0.4 | MIT |
+| @mediapipe/tasks-vision | 0.10.17 | Apache-2.0 |
+| @monogrid/gainmap-js | 3.7.3 | MIT |
+| @nodelib/fs.scandir | 2.1.5 | MIT |
+| @nodelib/fs.stat | 2.0.5 | MIT |
+| @nodelib/fs.walk | 1.2.8 | MIT |
+| @octokit/app | 16.1.4 | MIT |
+| @octokit/auth-app | 8.3.1 | MIT |
+| @octokit/auth-oauth-app | 9.0.5 | MIT |
+| @octokit/auth-oauth-device | 8.0.5 | MIT |
+| @octokit/auth-oauth-user | 6.0.4 | MIT |
+| @octokit/auth-token | 6.0.0 | MIT |
+| @octokit/auth-unauthenticated | 7.0.5 | MIT |
+| @octokit/core | 7.0.8 | MIT |
+| @octokit/endpoint | 11.0.5 | MIT |
+| @octokit/graphql | 9.0.5 | MIT |
+| @octokit/oauth-app | 8.0.4 | MIT |
+| @octokit/oauth-authorization-url | 8.0.0 | MIT |
+| @octokit/oauth-methods | 6.0.5 | MIT |
+| @octokit/openapi-types | 29.0.1 | MIT |
+| @octokit/openapi-types | 27.0.0 | MIT |
+| @octokit/openapi-types | 28.0.0 | MIT |
+| @octokit/openapi-webhooks-types | 12.1.0 | MIT |
+| @octokit/plugin-paginate-graphql | 6.0.0 | MIT |
+| @octokit/plugin-paginate-rest | 14.0.0 | MIT |
+| @octokit/plugin-paginate-rest | 15.0.0 | MIT |
+| @octokit/plugin-rest-endpoint-methods | 17.0.0 | MIT |
+| @octokit/plugin-retry | 8.1.1 | MIT |
+| @octokit/plugin-throttling | 11.0.5 | MIT |
+| @octokit/request | 10.0.16 | MIT |
+| @octokit/request-error | 7.1.2 | MIT |
+| @octokit/types | 18.0.0 | MIT |
+| @octokit/types | 16.0.0 | MIT |
+| @octokit/types | 17.0.0 | MIT |
+| @octokit/webhooks | 14.2.0 | MIT |
+| @octokit/webhooks-methods | 6.0.0 | MIT |
+| @pinojs/redact | 0.4.0 | MIT |
+| @radix-ui/number | 1.1.3 | MIT |
+| @radix-ui/primitive | 1.2.0 | MIT |
+| @radix-ui/react-accessible-icon | 1.2.0 | MIT |
+| @radix-ui/react-accordion | 1.3.1 | MIT |
+| @radix-ui/react-alert-dialog | 1.2.1 | MIT |
+| @radix-ui/react-arrow | 1.1.17 | MIT |
+| @radix-ui/react-aspect-ratio | 1.2.0 | MIT |
+| @radix-ui/react-avatar | 1.3.1 | MIT |
+| @radix-ui/react-checkbox | 1.4.1 | MIT |
+| @radix-ui/react-collapsible | 1.2.1 | MIT |
+| @radix-ui/react-collection | 1.2.0 | MIT |
+| @radix-ui/react-compose-refs | 1.1.5 | MIT |
+| @radix-ui/react-context | 1.3.0 | MIT |
+| @radix-ui/react-context-menu | 2.4.1 | MIT |
+| @radix-ui/react-dialog | 1.3.1 | MIT |
+| @radix-ui/react-direction | 1.2.0 | MIT |
+| @radix-ui/react-dismissable-layer | 1.2.1 | MIT |
+| @radix-ui/react-dropdown-menu | 2.2.1 | MIT |
+| @radix-ui/react-focus-guards | 1.2.0 | MIT |
+| @radix-ui/react-focus-scope | 1.3.1 | MIT |
+| @radix-ui/react-form | 0.3.0 | MIT |
+| @radix-ui/react-hover-card | 1.2.1 | MIT |
+| @radix-ui/react-id | 1.1.4 | MIT |
+| @radix-ui/react-label | 2.1.17 | MIT |
+| @radix-ui/react-menu | 2.2.1 | MIT |
+| @radix-ui/react-menubar | 1.2.1 | MIT |
+| @radix-ui/react-navigation-menu | 1.4.1 | MIT |
+| @radix-ui/react-one-time-password-field | 0.2.1 | MIT |
+| @radix-ui/react-password-toggle-field | 0.2.1 | MIT |
+| @radix-ui/react-popover | 1.3.1 | MIT |
+| @radix-ui/react-popper | 1.4.0 | MIT |
+| @radix-ui/react-portal | 1.2.0 | MIT |
+| @radix-ui/react-presence | 1.1.11 | MIT |
+| @radix-ui/react-primitive | 2.2.0 | MIT |
+| @radix-ui/react-progress | 1.2.1 | MIT |
+| @radix-ui/react-radio-group | 1.5.1 | MIT |
+| @radix-ui/react-roving-focus | 1.2.1 | MIT |
+| @radix-ui/react-scroll-area | 1.4.0 | MIT |
+| @radix-ui/react-select | 2.4.1 | MIT |
+| @radix-ui/react-separator | 1.2.0 | MIT |
+| @radix-ui/react-slider | 1.6.1 | MIT |
+| @radix-ui/react-slot | 1.5.0 | MIT |
+| @radix-ui/react-switch | 1.4.1 | MIT |
+| @radix-ui/react-tabs | 1.2.1 | MIT |
+| @radix-ui/react-toast | 1.3.1 | MIT |
+| @radix-ui/react-toggle | 1.2.1 | MIT |
+| @radix-ui/react-toggle-group | 1.2.1 | MIT |
+| @radix-ui/react-toolbar | 1.2.1 | MIT |
+| @radix-ui/react-tooltip | 1.4.1 | MIT |
+| @radix-ui/react-use-callback-ref | 1.1.5 | MIT |
+| @radix-ui/react-use-controllable-state | 1.3.0 | MIT |
+| @radix-ui/react-use-effect-event | 0.0.5 | MIT |
+| @radix-ui/react-use-escape-keydown | 1.1.6 | MIT |
+| @radix-ui/react-use-is-hydrated | 0.1.3 | MIT |
+| @radix-ui/react-use-layout-effect | 1.1.4 | MIT |
+| @radix-ui/react-use-previous | 1.1.4 | MIT |
+| @radix-ui/react-use-rect | 1.1.4 | MIT |
+| @radix-ui/react-use-size | 1.1.5 | MIT |
+| @radix-ui/react-visually-hidden | 1.2.13 | MIT |
+| @radix-ui/rect | 1.1.3 | MIT |
+| @react-three/drei | 10.7.9 | MIT |
+| @react-three/fiber | 9.8.2 | MIT |
+| @remix-run/route-pattern | 0.22.1 | MIT |
+| @tanstack/query-core | 5.107.1 | MIT |
+| @tanstack/react-query | 5.107.1 | MIT |
+| @tweenjs/tween.js | 23.1.3 | MIT |
+| @types/aws-lambda | 8.10.164 | MIT |
+| @types/d3-color | 3.1.3 | MIT |
+| @types/d3-drag | 3.0.7 | MIT |
+| @types/d3-interpolate | 3.0.4 | MIT |
+| @types/d3-selection | 3.0.12 | MIT |
+| @types/d3-transition | 3.0.9 | MIT |
+| @types/d3-zoom | 3.0.9 | MIT |
+| @types/debug | 4.1.13 | MIT |
+| @types/draco3d | 1.4.10 | MIT |
+| @types/estree | 1.0.10 | MIT |
+| @types/estree-jsx | 1.0.5 | MIT |
+| @types/hast | 3.0.5 | MIT |
+| @types/mdast | 4.0.4 | MIT |
+| @types/ms | 2.1.0 | MIT |
+| @types/offscreencanvas | 2019.7.3 | MIT |
+| @types/react-reconciler | 0.28.9 | MIT |
+| @types/stats.js | 0.17.4 | MIT |
+| @types/three | 0.187.0 | MIT |
+| @types/unist | 3.0.3 | MIT |
+| @types/unist | 2.0.11 | MIT |
+| @types/webxr | 0.5.24 | MIT |
+| @ungap/structured-clone | 1.4.0 | ISC |
+| @use-gesture/core | 10.3.1 | MIT |
+| @use-gesture/react | 10.3.1 | MIT |
+| @xyflow/react | 12.12.1 | MIT |
+| @xyflow/system | 0.0.84 | MIT |
+| abstract-logging | 2.0.1 | MIT |
+| ajv | 8.20.0 | MIT |
+| ajv-formats | 3.0.1 | MIT |
+| aria-hidden | 1.2.6 | MIT |
+| atomic-sleep | 1.0.0 | MIT |
+| avvio | 9.3.1 | MIT |
+| bail | 2.0.2 | MIT |
+| balanced-match | 4.0.4 | MIT |
+| base64-js | 1.5.1 | MIT |
+| before-after-hook | 4.0.0 | Apache-2.0 |
+| bidi-js | 1.1.0 | MIT |
+| bottleneck | 2.19.5 | MIT |
+| brace-expansion | 5.0.13 | MIT |
+| braces | 3.0.3 | MIT |
+| buffer | 6.0.3 | MIT |
+| camera-controls | 3.1.2 | MIT |
+| ccount | 2.0.1 | MIT |
+| character-entities | 2.0.2 | MIT |
+| character-entities-html4 | 2.1.0 | MIT |
+| character-entities-legacy | 3.0.0 | MIT |
+| character-reference-invalid | 2.0.1 | MIT |
+| classcat | 5.0.5 | MIT |
+| cmdk | 1.1.1 | MIT |
+| codemirror | 6.0.2 | MIT |
+| comma-separated-tokens | 2.0.3 | MIT |
+| content-disposition | 3.0.0 | MIT |
+| content-type | 3.1.1 | MIT |
+| cookie | 2.0.2 | MIT |
+| cookie-es | 3.1.1 | MIT |
+| crelt | 1.0.7 | MIT |
+| cross-env | 7.0.3 | MIT |
+| cross-spawn | 7.0.6 | MIT |
+| d3-binarytree | 1.0.2 | MIT |
+| d3-color | 3.1.1 | ISC |
+| d3-dispatch | 3.0.2 | ISC |
+| d3-drag | 3.0.1 | ISC |
+| d3-ease | 3.0.2 | BSD-3-Clause |
+| d3-force-3d | 3.0.6 | MIT |
+| d3-interpolate | 3.0.2 | ISC |
+| d3-octree | 1.1.0 | MIT |
+| d3-quadtree | 3.0.2 | ISC |
+| d3-selection | 3.0.1 | ISC |
+| d3-timer | 3.0.2 | ISC |
+| d3-transition | 3.0.2 | ISC |
+| d3-zoom | 3.0.1 | ISC |
+| debug | 4.4.3 | MIT |
+| decode-named-character-reference | 1.3.0 | MIT |
+| depd | 2.0.0 | MIT |
+| dequal | 2.0.3 | MIT |
+| detect-gpu | 5.0.70 | MIT |
+| detect-node-es | 1.1.0 | MIT |
+| devlop | 1.1.0 | MIT |
+| draco3d | 1.5.9 | Apache-2.0 |
+| escape-html | 1.0.3 | MIT |
+| estree-util-is-identifier-name | 3.0.0 | MIT |
+| events | 3.3.0 | MIT |
+| extend | 3.0.2 | MIT |
+| fast-decode-uri-component | 1.0.1 | MIT |
+| fast-deep-equal | 3.1.3 | MIT |
+| fast-glob | 3.3.3 | MIT |
+| fast-json-stringify | 7.3.5 | MIT |
+| fast-querystring | 1.1.2 | MIT |
+| fast-uri | 4.2.3 | BSD-3-Clause |
+| fast-uri | 3.1.9 | BSD-3-Clause |
+| fastify | 5.12.8 | MIT |
+| fastify-plugin | 6.0.1 | MIT |
+| fastq | 1.20.3 | ISC |
+| fflate | 0.6.11 | MIT |
+| fflate | 0.8.3 | MIT |
+| fill-range | 7.1.1 | MIT |
+| find-my-way | 9.10.0 | MIT |
+| get-nonce | 1.0.1 | MIT |
+| glob | 14.0.5 | BlueOak-1.0.0 |
+| glob-parent | 5.1.2 | ISC |
+| glsl-noise | 0.0.0 | MIT |
+| graphology | 0.26.0 | MIT |
+| graphology-communities-louvain | 2.0.2 | MIT |
+| graphology-indices | 0.17.1 | MIT |
+| graphology-layout-forceatlas2 | 0.10.1 | MIT |
+| graphology-layout-noverlap | 0.4.2 | MIT |
+| graphology-types | 0.24.8 | MIT |
+| graphology-utils | 2.5.2 | MIT |
+| hast-util-to-jsx-runtime | 2.3.7 | MIT |
+| hast-util-whitespace | 3.0.0 | MIT |
+| hls.js | 1.7.4 | Apache-2.0 |
+| html-to-image | 1.11.13 | MIT |
+| html-url-attributes | 3.0.1 | MIT |
+| http-errors | 2.0.1 | MIT |
+| ieee754 | 1.2.1 | BSD-3-Clause |
+| ignore | 7.0.14 | MIT |
+| immediate | 3.0.6 | MIT |
+| inherits | 2.0.4 | ISC |
+| inline-style-parser | 0.2.9 | MIT |
+| ipaddr.js | 2.5.0 | MIT |
+| is-alphabetical | 2.0.1 | MIT |
+| is-alphanumerical | 2.0.1 | MIT |
+| is-decimal | 2.0.1 | MIT |
+| is-extglob | 2.1.1 | MIT |
+| is-glob | 4.0.3 | MIT |
+| is-hexadecimal | 2.0.1 | MIT |
+| is-number | 7.0.0 | MIT |
+| is-plain-obj | 4.1.0 | MIT |
+| is-promise | 2.2.2 | MIT |
+| isexe | 2.0.0 | ISC |
+| its-fine | 2.1.1 | MIT |
+| json-schema-ref-resolver | 3.0.1 | MIT |
+| json-schema-traverse | 1.0.0 | MIT |
+| json-with-bigint | 3.5.12 | MIT |
+| lie | 3.3.0 | MIT |
+| light-my-request | 6.8.0 | BSD-3-Clause |
+| longest-streak | 3.1.0 | MIT |
+| lru-cache | 11.5.3 | BlueOak-1.0.0 |
+| lucide-react | 1.72.0 | ISC |
+| maath | 0.10.8 | MIT |
+| mdast-util-from-markdown | 2.1.0 | MIT |
+| mdast-util-mdx-expression | 2.0.1 | MIT |
+| mdast-util-mdx-jsx | 3.2.1 | MIT |
+| mdast-util-mdxjs-esm | 2.0.1 | MIT |
+| mdast-util-phrasing | 4.1.0 | MIT |
+| mdast-util-to-hast | 13.2.1 | MIT |
+| mdast-util-to-markdown | 2.2.3 | MIT |
+| mdast-util-to-string | 4.0.0 | MIT |
+| merge2 | 1.4.1 | MIT |
+| meshline | 3.3.1 | MIT |
+| meshoptimizer | 1.1.1 | MIT |
+| micromark | 4.0.3 | MIT |
+| micromark-core-commonmark | 2.0.4 | MIT |
+| micromark-factory-destination | 2.0.1 | MIT |
+| micromark-factory-label | 2.0.1 | MIT |
+| micromark-factory-space | 2.1.0 | MIT |
+| micromark-factory-title | 2.0.1 | MIT |
+| micromark-factory-whitespace | 2.0.1 | MIT |
+| micromark-util-character | 2.1.1 | MIT |
+| micromark-util-chunked | 2.0.1 | MIT |
+| micromark-util-classify-character | 2.0.1 | MIT |
+| micromark-util-combine-extensions | 2.0.1 | MIT |
+| micromark-util-decode-numeric-character-reference | 2.0.2 | MIT |
+| micromark-util-decode-string | 2.0.1 | MIT |
+| micromark-util-edit-map | 1.0.0 | MIT |
+| micromark-util-encode | 2.0.1 | MIT |
+| micromark-util-html-tag-name | 2.0.1 | MIT |
+| micromark-util-normalize-identifier | 2.0.1 | MIT |
+| micromark-util-resolve-all | 2.0.1 | MIT |
+| micromark-util-sanitize-uri | 2.0.1 | MIT |
+| micromark-util-subtokenize | 2.1.0 | MIT |
+| micromark-util-symbol | 2.0.1 | MIT |
+| micromark-util-types | 2.0.3 | MIT |
+| micromatch | 4.0.8 | MIT |
+| mime | 3.0.0 | MIT |
+| minimatch | 11.0.8 | BlueOak-1.0.0 |
+| minipass | 7.1.3 | BlueOak-1.0.0 |
+| mnemonist | 0.39.8 | MIT |
+| ms | 2.1.3 | MIT |
+| obliterator | 2.0.5 | MIT |
+| octokit | 5.0.5 | MIT |
+| on-exit-leak-free | 2.1.2 | MIT |
+| pandemonium | 2.4.1 | MIT |
+| parse-entities | 4.0.2 | MIT |
+| path-key | 3.1.1 | MIT |
+| path-scurry | 3.0.0 | BlueOak-1.0.0 |
+| pend | 1.2.0 | MIT |
+| picomatch | 2.3.2 | MIT |
+| pino | 10.4.0 | MIT |
+| pino-abstract-transport | 3.0.0 | MIT |
+| pino-std-serializers | 7.1.0 | MIT |
+| potpack | 1.0.2 | ISC |
+| process-warning | 5.1.1 | MIT |
+| promise-worker-transferable | 1.0.4 | Apache-2.0 |
+| property-information | 7.2.0 | MIT |
+| queue-microtask | 1.2.3 | MIT |
+| quick-format-unescaped | 4.0.4 | MIT |
+| radix-ui | 1.8.1 | MIT |
+| react | 19.3.1 | MIT |
+| react-dom | 19.3.1 | MIT |
+| react-markdown | 10.1.0 | MIT |
+| react-remove-scroll | 2.7.2 | MIT |
+| react-remove-scroll-bar | 2.3.8 | MIT |
+| react-router | 8.5.1 | MIT |
+| react-style-singleton | 2.2.3 | MIT |
+| react-use-measure | 2.1.7 | MIT |
+| real-require | 1.0.0 | MIT |
+| remark-parse | 11.0.0 | MIT |
+| remark-rehype | 11.1.2 | MIT |
+| require-from-string | 2.0.2 | MIT |
+| ret | 0.5.1 | MIT |
+| reusify | 1.1.0 | MIT |
+| rfdc | 1.4.1 | MIT |
+| run-parallel | 1.2.0 | MIT |
+| safe-regex2 | 5.1.2 | MIT |
+| safe-stable-stringify | 2.5.0 | MIT |
+| scheduler | 0.28.0 | MIT |
+| secure-json-parse | 4.1.1 | BSD-3-Clause |
+| semver | 7.8.6 | ISC |
+| set-cookie-parser | 3.2.2 | MIT |
+| setprototypeof | 1.2.0 | ISC |
+| shebang-command | 2.0.0 | MIT |
+| shebang-regex | 3.0.0 | MIT |
+| sigma | 3.0.3 | MIT |
+| sonic-boom | 4.2.1 | MIT |
+| space-separated-tokens | 2.0.2 | MIT |
+| split2 | 4.2.0 | ISC |
+| stats-gl | 2.4.2 | MIT |
+| stats.js | 0.17.0 | MIT |
+| statuses | 2.0.2 | MIT |
+| stringify-entities | 4.0.4 | MIT |
+| style-mod | 4.1.4 | MIT |
+| style-to-object | 2.0.2 | MIT |
+| suspend-react | 0.1.3 | MIT |
+| thread-stream | 4.2.1 | MIT |
+| three | 0.187.0 | MIT |
+| three | 0.170.0 | MIT |
+| three-mesh-bvh | 0.8.3 | MIT |
+| three-stdlib | 2.36.1 | MIT |
+| to-regex-range | 5.0.1 | MIT |
+| toad-cache | 3.7.4 | MIT |
+| toidentifier | 1.0.1 | MIT |
+| trim-lines | 3.0.1 | MIT |
+| troika-three-text | 0.52.5 | MIT |
+| troika-three-utils | 0.52.5 | MIT |
+| troika-worker-utils | 0.52.0 | MIT |
+| trough | 2.2.0 | MIT |
+| tslib | 2.9.1 | 0BSD |
+| tunnel-rat | 0.1.2 | MIT |
+| typescript | 5.9.3 | Apache-2.0 |
+| unified | 11.0.5 | MIT |
+| unist-util-is | 6.0.1 | MIT |
+| unist-util-position | 5.0.0 | MIT |
+| unist-util-stringify-position | 4.0.0 | MIT |
+| unist-util-visit | 5.2.0 | MIT |
+| unist-util-visit-parents | 6.1.0 | MIT |
+| universal-github-app-jwt | 2.2.2 | MIT |
+| universal-user-agent | 7.0.3 | ISC |
+| use-callback-ref | 1.3.3 | MIT |
+| use-sidecar | 1.1.3 | MIT |
+| use-sync-external-store | 1.7.0 | MIT |
+| utility-types | 3.11.0 | MIT |
+| vfile | 6.0.3 | MIT |
+| vfile-message | 4.0.4 | MIT |
+| w3c-keyname | 2.2.8 | MIT |
+| web-tree-sitter | 0.27.2 | MIT |
+| webgl-constants | 1.1.1 | UNKNOWN |
+| webgl-sdf-generator | 1.1.1 | MIT |
+| which | 2.0.2 | ISC |
+| yauzl | 3.4.0 | MIT |
+| zod | 4.6.7 | MIT |
+| zustand | 5.1.0 | MIT |
+| zustand | 4.5.7 | MIT |
+| zwitch | 2.0.4 | MIT |
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
new file mode 100644
index 0000000..6d8a203
--- /dev/null
+++ b/docs/PERFORMANCE.md
@@ -0,0 +1,108 @@
+# Performance
+
+VECTORA is meant to stay usable on ordinary computers and does not assume a powerful GPU. This page records what was measured, where the time goes, and which design decisions keep resource use bounded.
+
+## Benchmarks
+
+Run with `npm run bench` (`scripts/bench.ts`). The raw results are in [benchmarks/latest.json](benchmarks/latest.json).
+
+**These numbers describe one machine on one day. They are measurements, not promises**; other hardware, other repositories and other disks will differ.
+
+- Machine: 12th Gen Intel Core i5-12450H (12 logical cores), 7.7 GB RAM, Windows 11, Node.js 24.14.1, SSD.
+- Date: 2026-11-12. Indexing workers: 4.
+- Repositories are synthetic: 70% TypeScript and 30% Python files, each with a class, several functions and constants, importing and calling two or three other files. Real repositories have larger and more varied files.
+- Peak memory is the resident set size of the engine process, which includes its worker threads. The separate Python interpreter process is not included.
+- CPU is process CPU time divided by wall time across all 12 logical cores (8.3% equals one core fully used).
+
+### Indexing
+
+| Files | Nodes | Symbols | Relationships | Full index | Peak memory | CPU | Re-index, nothing changed | Re-index, one TypeScript file changed |
+|---:|---:|---:|---:|---:|---:|---:|---:|---:|
+| 1,000 | 7,948 | 6,700 | 26,949 | 5.6 s | 488 MB | 8.8% | 1.1 s | 5.7 s |
+| 5,000 | 39,033 | 33,500 | 134,053 | 22.3 s | 706 MB | 6.1% | 5.1 s | 17.5 s |
+| 10,000 | 77,758 | 67,000 | 267,793 | 31.3 s | 995 MB | 6.8% | 10.6 s | 27.2 s |
+
+All three runs finished with 0 validation errors and 0 unresolved references (the synthetic code is fully resolvable). An earlier run of the 10,000-file case on a cold file cache took 54 s; the figure above is from a warm cache.
+
+In the one-file case 9 files were re-analysed (the changed file and its dependants) and all others reused.
+
+### Storing and loading the graph
+
+| Files | Rows (nodes + relationships) | Save to SQLite | Load from SQLite | Database size |
+|---:|---:|---:|---:|---:|
+| 1,000 | 34,897 | 0.8 s | 0.2 s | 29 MB |
+| 5,000 | 173,086 | 4.4 s | 1.3 s | 150 MB |
+| 10,000 | 345,551 | 8.9 s | 2.5 s | 304 MB |
+
+### Graph of 50,000 symbols and 100,000 relationships
+
+A graph generated directly (52,521 nodes including 2,500 files and 20 subsystems; 152,500 relationships in total including containment).
+
+| Operation | Time |
+|---|---:|
+| Validate | 0.54 s |
+| Build adjacency index | 0.17 s |
+| Community detection (Louvain) | 0.86 s |
+| Cycle detection | 0.12 s |
+| Build search index | 0.10 s |
+| 50 search queries | 2.8 s (about 56 ms each) |
+| 3D layout of all 52,521 nodes | 3.9 s |
+| Save to SQLite | 8.0 s |
+| Load from SQLite | 1.3 s |
+| Load structure only (no symbols) | 0.7 s |
+| Export JSON (58 MB) | 0.16 s |
+
+This pass ran in the same process straight after indexing, so its memory figures (about 1.1 GB) include what the earlier runs left behind and are not reported as a separate measurement.
+
+### What was not measured
+
+- **Initial graph rendering time and interaction responsiveness in the browser.** There is no automated rendering benchmark yet. The browser tests confirm that the three views render and respond for the sample repository; they do not measure frame times.
+- **Memory growth over long sessions and release of WebGL resources.** Geometries created by hand are disposed when replaced and React Three Fiber disposes the rest on unmount, but this has not been verified with a memory profiler.
+- **Repositories larger than 10,000 files**, and repositories that are 100% TypeScript at that size.
+
+These gaps are listed in [../KNOWN_ISSUES.md](../KNOWN_ISSUES.md).
+
+## Where the time goes
+
+Measured stage by stage on the 1,000-file repository (3.6 s in this run): discovery 0.6 s, syntax analysis 2.6 s, resolution 0.09 s, flows and organisation 0.07 s, graph generation and validation 0.2 s. With every file reused from the cache the whole pipeline takes 0.7 s.
+
+1. **TypeScript analysis** is the largest cost. One compiler program covers all TypeScript and JavaScript files and runs in a single worker, which is why CPU use stays near one core. The type checker cannot be split across threads.
+2. **One changed TypeScript file costs almost as much as a full index.** The cache avoids walking unchanged files, but the compiler program is created again from scratch, and that dominates. Changes to Python and tier-2 files do not have this cost: only the changed files are parsed. Reusing the compiler program between runs is on the roadmap.
+3. **Reading and hashing files** dominates the unchanged case (10.6 s for 10,000 files on this machine, where every file read passes through real-time antivirus scanning).
+4. **Saving** takes about 25 microseconds per row; the JSON of each node and relationship is stored whole.
+
+## Optimisation decisions
+
+### Indexing
+
+- **Bounded concurrency.** A worker pool sized by the *Indexing workers* setting; jobs run one at a time, so two imports never compete for memory.
+- **Worker threads** for Tree-sitter batches and the TypeScript program; a separate interpreter process for Python. The engine's main thread stays free to answer requests.
+- **Per-file cache keyed by content hash.** Unchanged files are not analysed again.
+- **Unchanged fast path.** A fingerprint over every file's path and hash lets a re-index of an unchanged repository return right after the scan, keeping the stored graph.
+- **Limits.** Maximum file size (512 KB), maximum files per repository (20,000), both configurable. Binary, generated and minified files are skipped.
+- **No dependency installation, no script execution**, so indexing cost depends only on the source text.
+- **The compiler reads only indexed files.** Module resolution is done against the list of indexed files, so `node_modules` is never parsed.
+
+### Storage and memory
+
+- SQLite in WAL mode, multi-row inserts inside one transaction per table, ordinary row-id tables (an earlier `WITHOUT ROWID` layout was measurably slower for large JSON rows and was replaced).
+- At most two repositories are held in memory by the engine.
+- **Progressive loading.** When a repository has more symbols than *Maximum nodes on screen* (8,000 by default), the browser first loads the structural graph (repository, subsystems, folders, files, packages, concepts) and fetches the symbols of a file when it is unfolded.
+
+### Rendering
+
+- **Node density.** *Structure* shows files only; *Balanced* adds the prominent symbol kinds; *Full* shows everything up to the node limit.
+- **2D:** WebGL rendering through Sigma; labels by zoom and density, never all at once; edges and labels hidden while moving above 5,000 elements; layout in a Web Worker.
+- **3D:** one instanced mesh per shape family; relationships in two buffer geometries; no React component per node or edge; no per-frame React state; on-demand rendering so an idle scene costs nothing; cross-file relationships only when viewed from far away; a label budget with frustum and distance culling; lower geometry detail, no antialiasing and no labels at *Low* quality; pointer hover disabled above 20,000 nodes; layout in a Web Worker.
+- **Mind map:** only expanded branches exist as elements; off-screen elements are culled above 600 visible nodes.
+- **Fallback.** Without WebGL the Atlas offers the 2D view; the Mind Map, search and inspector need no WebGL.
+- **Reduced motion** removes animated layout settling and camera transitions.
+- **No background work.** No polling outside an active indexing job, no file watching, no network requests except those the user starts.
+
+### Loading the application
+
+The main bundle is about 690 kB (209 kB compressed). The three renderers and the source editor are separate chunks loaded when first used. Fonts are bundled as Latin subsets.
+
+## If VECTORA feels slow
+
+See “Indexing is slow or uses a lot of memory” and “The graph is crowded or slow to draw” in [TROUBLESHOOTING.md](TROUBLESHOOTING.md).
diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md
new file mode 100644
index 0000000..b38e4bf
--- /dev/null
+++ b/docs/PRODUCT.md
@@ -0,0 +1,62 @@
+# Product
+
+**VECTORA** — Understand what connects.
+
+VECTORA is a codebase intelligence platform that transforms software repositories into structured, interactive knowledge maps. It reveals how components, modules, dependencies and concepts connect, giving developers a new way to understand complex software systems.
+
+## Who it is for
+
+- A developer joining a project who needs to understand an unfamiliar repository without reading hundreds of files.
+- A maintainer assessing what a change will affect.
+- A reviewer or architect who wants to see dependencies between parts of a system, circular imports, and where requests reach data.
+
+## Principles
+
+Local-first. Privacy-conscious. Evidence-based. Fast and resource-efficient. Language-aware. Incrementally indexable. Visually restrained. Accessible. Modular. Extensible.
+
+## Features in version 0.1
+
+| Area | Feature |
+|---|---|
+| Import | GitHub repository (public; private with a token), ZIP archive, local folder, bundled sample |
+| Indexing | Eight-stage pipeline; TypeScript, JavaScript, Python (tier 1); Java, Go, Rust, C, C++, C# (tier 2); incremental re-indexing; worker threads |
+| Graph | 18 node types, 15 relationship types, evidence and confidence on each, validation, unresolved-reference reporting |
+| Exploration | Graph Explorer (2D), 3D Atlas, Mind Map, with shared selection |
+| Inspection | Inspector with original source, relationships and their evidence |
+| Search | Command palette, search page, documentation search |
+| Questions | Callers, dependents, impact, traces, data access, cycles, startup, explanations — all with citations |
+| Dependencies | Packages, subsystem dependencies, circular imports, flows, unresolved references |
+| Export | JSON, GraphML, Markdown analysis, architecture document, SVG, PNG |
+| Workspace | Saved and restored automatically |
+| Settings | Appearance, graph, performance, repository, privacy |
+| Themes | Light and dark |
+
+## User journeys
+
+1. **First look.** Open VECTORA → Explore a repository → index the sample → read the overview → open the Graph Explorer.
+2. **Understand a function.** Ctrl+K → type its name → Enter → read its source and relationships in the inspector → follow a caller.
+3. **Assess a change.** Search → Ask “What could be affected if I change X?” → highlight the result in the graph.
+4. **Follow a request.** Ask “Trace GET /notes” or open Dependency Explorer → Execution flows → show in graph.
+5. **See the shape.** 3D Atlas → global view → click a subsystem → show connected neighbourhood.
+6. **Document.** Export the architecture document and the mind map.
+7. **Return later.** Reopen the repository: selection, layout and filters are as you left them. Re-index after pulling changes.
+
+## In scope
+
+Static analysis of source text; structure and relationships that can be shown with evidence; local storage; single-user use on one machine.
+
+## Out of scope for 0.1
+
+- Running or building the analysed code, dynamic analysis, test coverage.
+- Hosted or multi-user operation, authentication, sharing links.
+- Git history, blame, diffs between commits.
+- Editing code.
+- A packaged desktop application (planned; see [ROADMAP.md](ROADMAP.md)).
+- Languages other than the nine listed. Other files are indexed as files only.
+
+## What VECTORA does not claim
+
+- It does not claim to support every language or framework.
+- It does not claim that derived groupings (concepts, flows, data-access relationships) are facts; they are marked as derived.
+- It does not claim that the absence of a relationship means no relationship exists: dynamic dispatch, reflection and unresolved references are outside what static structure shows. Unresolved references are counted and listed.
+- Benchmark figures describe one machine and are not promises.
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
new file mode 100644
index 0000000..4354fd0
--- /dev/null
+++ b/docs/ROADMAP.md
@@ -0,0 +1,38 @@
+# Roadmap
+
+Items are grouped by intent, not by date. Nothing here is a commitment.
+
+## Next
+
+- **Operating-system credential store.** Keep the vault key in Windows Credential Manager, macOS Keychain or the Secret Service instead of a file.
+- **Faster incremental TypeScript indexing.** Reuse the previous compiler program between runs inside a long-lived worker so that unchanged files are not parsed again.
+- **Receiver type inference for Python and tier-2 languages**, so that calls on local variables with an evident type (`x = Foo(); x.bar()`) can be resolved.
+- **Watch mode** for local folders, off by default, re-indexing after changes settle.
+- **Node shapes in the 2D view** to match the 3D shapes.
+- **Screenshots** in the user guide.
+- **Mounted routers and route prefixes** (`app.use('/api', router)`) reflected in endpoint paths.
+- **Rendering benchmarks** measured in a browser (frame times while panning and zooming large graphs).
+
+## Later
+
+- More languages through Tree-sitter: Kotlin, Swift, Ruby, PHP.
+- Git awareness: changed-since, ownership, comparison between two indexed states.
+- Search inside source text, not only names and documentation.
+- Saved, named views and annotations on nodes.
+- Import from other hosts (GitLab, Bitbucket).
+
+## Desktop packaging plan (Tauri 2)
+
+The first release is a web application served by the local engine. A desktop build is planned as an optional distribution and is **not built yet**; no desktop artifact exists.
+
+**Approach.** A Tauri 2 shell with the engine as a sidecar process.
+
+1. **Sidecar.** Package the engine bundle (`dist/engine`) together with a Node.js runtime as a sidecar binary, or compile it to a single executable with the single-executable-application support in Node.js. `typescript` and `web-tree-sitter` load files from disk at run time, so they are shipped as resources next to the sidecar. Open question to settle first: single-executable packaging with those two packages has not been tried.
+2. **Shell.** `src-tauri` with one window loading the built web application (`dist/web`). On start the shell picks a free loopback port, launches the sidecar with `VECTORA_PORT` and a data directory under the per-user application data folder, waits for `/api/health`, then opens the window at `http://127.0.0.1:`.
+3. **Permissions.** Tauri capabilities limited to: starting the sidecar, the dialog plugin for a native folder picker (replacing the built-in folder browser), and opening external links. No general file-system or shell access for the web content.
+4. **Engine origin checks.** Add the Tauri origin to `VECTORA_ALLOWED_ORIGINS` for the sidecar only.
+5. **Python.** Keep using the system interpreter and show the existing notice when none is found; do not bundle one.
+6. **Updates and signing.** Tauri updater with signed releases; Windows code signing and macOS notarisation are prerequisites for distribution.
+7. **Verification.** Reuse the Playwright journeys against the packaged application through the WebDriver support of Tauri.
+
+**Prerequisites on the build machine.** Rust toolchain, the platform webview (WebView2 on Windows) and the Tauri CLI. The repository does not contain `src-tauri` yet.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..e270b86
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,55 @@
+# Security and privacy
+
+Repository contents can be confidential. VECTORA is designed so that analysing a repository does not expose it, and so that a hostile repository or archive cannot harm the machine that analyses it.
+
+## What stays local, what leaves
+
+| Data | Where it goes |
+|---|---|
+| Source code, graph, analysis, search index, sessions, settings | Stays on this machine, in the data directory |
+| Access tokens | Stored encrypted in the data directory. Sent only to `api.github.com`, in the `Authorization` header |
+| GitHub imports | The engine requests repository metadata, branches and a source archive from `api.github.com`. Only when you start a GitHub import |
+| Optional narrative service (off by default) | When enabled: the question and the findings already shown on screen (symbol names, file paths, one short evidence line per finding) are sent to the endpoint you configured. Whole files are never sent |
+| Fonts, scripts, styles | Bundled. The application makes no requests to third-party hosts |
+
+There is no telemetry and no account.
+
+## Threat model
+
+| Threat | Protection |
+|---|---|
+| A web page in the user's browser calls the local engine | The engine binds to `127.0.0.1`. Requests whose `Host` is not a loopback name are refused (DNS rebinding). Requests with an `Origin` other than the engine's own or an allowed development origin are refused, as are state-changing requests marked cross-site. CORS allows only those origins |
+| A malicious ZIP archive (path traversal, absolute paths, symlinks, decompression bomb, device names, overlong names) | Archives are inspected before extraction. Entries with unsafe names are rejected; symbolic-link entries are skipped; each write is checked to stay inside the destination; files are created with exclusive-create so an entry cannot overwrite another; limits apply to entry count (100,000), total uncompressed size (2 GB), per-file size and compression ratio; uploads are limited to 512 MB |
+| A file that is not an archive | The signature is checked; the upload is deleted and the reason reported |
+| A repository that tries to run code during analysis | Nothing from the repository is executed. No scripts, no hooks, no dependency installation. Git is never invoked: GitHub repositories arrive as archives and a local branch name is read from `.git/HEAD` as a text file. Python sources are parsed, not imported, by an interpreter started in isolated mode |
+| Symbolic links pointing outside the repository | Not followed during scanning; link entries are skipped |
+| Reading arbitrary files through the source API | Only paths that are file nodes in the indexed graph are served; the resolved real path must stay inside the repository root; reads are capped at 2 MB |
+| Credential leakage | Tokens are accepted only in request bodies, never in URLs (a URL containing credentials is rejected with an explanation). Stored values are encrypted with AES-256-GCM. The credentials API returns names and dates only. Request bodies are never logged; authorization headers are removed from logs; log text passes through secret redaction. Exports and repository records contain no credentials |
+| Source text rendered as markup | Source and documentation are rendered as text. The Markdown renderer ignores raw HTML and only allows `http`, `https` and `mailto` links; images in repository documents are not loaded |
+| Private code leaving through an export | Graph exports omit evidence lines, signatures and doc comments unless source inclusion is requested explicitly; the export dialog warns when it is on |
+| Debug access in release builds | The engine has no debug, environment or configuration endpoints. Error responses carry a message only; stack traces are not logged in production mode |
+
+Response headers on every reply: a Content Security Policy restricted to the engine's own origin, `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, `Cross-Origin-Resource-Policy: same-origin`.
+
+## Credential storage
+
+The key is 32 random bytes in `vault.key` inside the data directory, created with owner-only permissions where the file system supports them. Each value is encrypted with AES-256-GCM and a fresh nonce.
+
+Limitation: the key is stored next to the database. This protects stored tokens against casual disclosure (a copied database file, a backup of the database alone, a log) but not against someone who can read the whole data directory as your user. Integration with the operating system's credential store is on the roadmap. On Windows the owner-only file mode is not enforced by the file system; rely on the permissions of your user profile or data folder.
+
+## Local engine exposure
+
+The folder browser used by the local import dialog lists directory names on this machine. It is reachable only through the loopback interface and is subject to the host and origin checks above. Do not expose the engine's port to a network: it has no authentication because it is meant to be reachable only from the same machine. Setting `VECTORA_HOST` to a non-loopback address is not supported.
+
+## Dependencies
+
+- `npm audit --omit=dev` reports no known vulnerabilities at the time of writing. An advisory in the static-file plugin found during development was resolved by upgrading it.
+- `npm run licenses` writes `docs/LICENSES.md`, the licence inventory of the production dependency closure.
+
+## Verification
+
+The checks above are covered by automated tests in `tests/integration/engine.test.ts`: traversal and unsafe names, symbolic links, oversized and invalid archives, entry and size limits, path containment, host and origin checks, absence of debug endpoints, credential handling (not in URLs, logs, responses, exports or in clear text in the database), and source-free exports.
+
+## Reporting a problem
+
+Open an issue describing the behaviour and how to reproduce it. Do not include private source code or tokens in the report.
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..680a131
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,105 @@
+# Testing
+
+All figures on this page come from runs on the development machine on 2026-11-12 (Windows 11, Node.js 24.14.1, Python 3.13, Microsoft Edge). Re-run the commands to get current results.
+
+## Commands
+
+| Command | What it runs |
+|---|---|
+| `npm run typecheck` | TypeScript in strict mode over every package, both applications, scripts and tests |
+| `npm run lint` | ESLint |
+| `npm test` | Unit and integration tests (Vitest) |
+| `npm run test:coverage` | The same, with V8 coverage |
+| `npm run build` then `npm run test:e2e` | Browser journeys (Playwright) against the production build |
+| `npm run bench` | Performance benchmarks (see [PERFORMANCE.md](PERFORMANCE.md)) |
+| `npm audit --omit=dev` | Known vulnerabilities in production dependencies |
+
+## Latest results
+
+| Suite | Result |
+|---|---|
+| Type check | No errors |
+| Lint | No errors, no warnings |
+| Unit and integration (`npm test`) | 79 passed, 0 failed, 4 files |
+| Browser journeys (`npm run test:e2e`) | 18 passed, 0 failed: 10 at desktop width, 4 at tablet width, 4 at mobile width |
+| Dependency audit | 0 known vulnerabilities |
+
+### Coverage
+
+From `npm run test:coverage`, over `packages/*/src`, `apps/local-engine/src` and `apps/web/src/lib`:
+
+| Metric | Covered |
+|---|---|
+| Statements | 80.5% |
+| Branches | 65.5% |
+| Functions | 79.3% |
+| Lines | 83.6% |
+
+React components and the three renderers are not part of this figure; they are exercised by the browser journeys.
+
+## What is tested
+
+### Unit tests — `tests/unit/indexing.test.ts` (26 tests)
+
+- **File scanning and ignore handling:** `.gitignore`, custom patterns, size limits, binary and generated detection, symbolic links not followed, file limit with preference for source files, language and configuration detection, manifests.
+- **Syntax and symbol extraction:** symbols, kinds, locations and doc comments in TypeScript; symbols in Python, Java, Go, Rust, C and C#; syntax errors reported without failing the index.
+- **Relationship resolution:** calls, imports, inheritance and routes through the type checker; evidence and location on every edge; pattern-based data access marked as derived; resolution through imports and scope in other languages; **no relationship invented for an undefined name; no resolution by name coincidence**.
+- **Graph validation:** the produced graph passes integrity checks; duplicates, dangling edges and missing evidence are detected; normalisation behaviour.
+- **Clustering:** subsystems, subsystem roots, concepts with evidence, entry points and flows, deterministic communities.
+- **Incremental indexing:** unchanged files reused; a renamed export re-analyses its dependants and leaves no stale relationship; worker threads give the same graph as in-process analysis.
+
+### Unit tests — `tests/unit/graph-engine.test.ts` (19 tests)
+
+Algorithms (cycles, traversal, shortest path, impact), search ranking and filters, **persistence** (round trip through SQLite across a reopen, settings validation, deletion), **export** (ids and relationships preserved; no source text unless requested; GraphML structure; Markdown documents), question exploration (intents, citations, “not found” instead of invention), layout (finite, deterministic, hierarchical), secret redaction.
+
+### Unit tests — `tests/unit/web.test.tsx` (14 tests, jsdom)
+
+Design tokens match the specified palette and the stylesheet; **WCAG contrast** of text and semantic colours in both themes; glyph, size and colour for every node type; `deriveView` (density, expansion, filters, focus, limits); inspector relations; the mind-map hierarchy built from the same graph; the shared selection store; form controls; the landing and About pages; user-facing copy free of excluded terminology.
+
+### Integration tests — `tests/integration/engine.test.ts` (20 tests)
+
+The engine is started in-process and driven through its HTTP interface.
+
+- **Local import**, full indexing, graph, structure-only graph, lazy members, overview, search, documentation, questions.
+- **Source access** for indexed files only, with traversal attempts refused.
+- **Incremental re-index**, including the unchanged fast path and a forced full run.
+- **Exports** in all formats.
+- **Workspace persistence:** session and settings survive an engine restart.
+- **ZIP import** through a multipart upload: nested root detection, ignored and binary entries left out, cleanup of uploads and of managed sources on removal; non-archives rejected.
+- **GitHub import** against a stand-in for `api.github.com` (no network access is used): public repository with branch and subdirectory; private repository refused without a credential and read with one.
+- **Security:** traversal and malicious names, symbolic links, oversized and invalid archives, entry and size limits, path containment, host and origin checks, no debug endpoints, credentials absent from URLs, logs, responses, exports and from the database in clear text; authenticated encryption of stored credentials; deletion of all data.
+
+### Browser journeys — `tests/e2e/journeys.spec.ts`
+
+Run against the production bundle with the installed Microsoft Edge (`channel: 'msedge'`; set `VECTORA_E2E_CHANNEL` to use another browser). Every journey also asserts that there were no console errors and no server errors.
+
+| Journey | Widths |
+|---|---|
+| Landing page, About, call to action | desktop, tablet, mobile |
+| Import screen: validation messages for all three methods, folder browser, sample import | desktop, tablet, mobile |
+| Workspace navigation to every view | desktop, tablet, mobile |
+| Search, graph selection, inspector with original source and evidence, focus mode, source view, filters | desktop |
+| Shared selection across Graph Explorer, 3D Atlas (orbit, zoom, reset, breadcrumb, neighbourhood, global view) and Mind Map | desktop |
+| Mind Map: expand, collapse, select, find, relationship overlay, SVG export | desktop |
+| Search view, questions with evidence links, Dependency Explorer, Documentation | desktop |
+| Settings persist and take effect, light and dark themes, JSON export contents, saved workspace restored | desktop |
+| Accessibility: skip link, landmarks, one `h1`, accessible names for all controls, labels for all fields, dialog focus, visible focus | desktop, tablet, mobile |
+| Reduced-motion preference | desktop |
+
+## What is not covered by automated tests
+
+- A live request to GitHub. The integration test uses a local stand-in; a real import depends on network access and on GitHub.
+- The optional narrative provider against a real service.
+- Screen-reader behaviour beyond names, roles and landmarks.
+- Visual regression (no screenshot comparison).
+- Rendering frame rates, memory growth over long sessions and WebGL resource release. Indexing and graph operations are benchmarked; rendering is not measured automatically.
+- Browsers other than Chromium-based Edge.
+- macOS and Linux. Development and all test runs were on Windows 11.
+
+These are listed in [../KNOWN_ISSUES.md](../KNOWN_ISSUES.md).
+
+## Notes for running the tests
+
+- Tests write only inside the repository folder (`.bench-tmp/`), never to the system drive.
+- The browser journeys need `npm run build` first; the setup stops with a clear message otherwise. The engine is started directly, without a shell, on port 4399 with its own empty data directory.
+- The sample repository in `samples/sample-repo` is the fixture for most tests. Changing it changes expected values in the tests.
diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md
new file mode 100644
index 0000000..d2904c4
--- /dev/null
+++ b/docs/TROUBLESHOOTING.md
@@ -0,0 +1,73 @@
+# Troubleshooting
+
+## The page says the local engine is not reachable
+
+The web application needs the engine. Start it with `npm start` (after `npm run build`) or `npm run dev`, then reload. If port 4317 is taken, set `VECTORA_PORT` to another port before starting.
+
+## `npm start` cannot find the server file
+
+Run `npm run build` first. `npm start` runs `dist/engine/server.mjs`.
+
+## A blank page after rebuilding
+
+The application shell is served without caching, but a browser tab opened before a rebuild may still hold old script files. Reload the tab; if it persists, do a hard reload (Ctrl+Shift+R).
+
+## Python files show no symbols
+
+VECTORA analyses Python with a Python 3 interpreter found as `python3`, `python` or `py -3`. Settings → Repository shows a notice when none is found. Install Python 3.8 or newer, or set `VECTORA_PYTHON` to the path of the interpreter, restart the engine and re-index.
+
+## Many references are “unresolved”
+
+That is expected for calls on values whose type cannot be known from the syntax alone (for example `client.send()` where `client` is a parameter) in Python and the tier-2 languages. VECTORA lists them instead of guessing. Dependency Explorer → Unresolved references groups them by reason. TypeScript and JavaScript resolve far more because the type checker is used.
+
+## A file or folder is missing from the graph
+
+Check, in order: your ignore patterns (Settings → Repository) and the `.gitignore` files of the repository; the maximum file size; whether the file is binary, generated or minified; and the file limit (Settings → Performance). The number of files skipped for each reason is part of the analysis (`/api/repositories//analysis`, field `skipped`).
+
+## Indexing is slow or uses a lot of memory
+
+- TypeScript analysis builds one compiler program for the whole repository in a single worker; it is the main cost for large TypeScript repositories. See [PERFORMANCE.md](PERFORMANCE.md) for measured figures.
+- Lower *Maximum files per repository*, add ignore patterns for folders you do not need (fixtures, generated code, vendored code), or import a subdirectory.
+- Keep *Indexing cache* and *Incremental indexing* on so later runs reuse earlier work.
+
+## The graph is crowded or slow to draw
+
+Set *Node density* to *Structure* (files only; unfold a file to see its symbols), hide relationship types in *Filters and legend*, use focus mode on a node, lower *Rendering quality* and *Label density*. Above the *Maximum nodes on screen* limit the graph loads progressively.
+
+## The 3D Atlas says 3D rendering is not available
+
+The browser has no WebGL (often a remote desktop session, a virtual machine, or hardware acceleration switched off). Use the 2D Graph Explorer, which shows the same graph, or enable hardware acceleration in the browser.
+
+## The 3D Atlas says it lost its graphics context
+
+The browser reclaimed graphics memory, usually under memory pressure or after the display driver restarted. Choose *Start the 3D view again*. Your selection and filters are kept.
+
+## A GitHub import fails
+
+| Message | Cause |
+|---|---|
+| Repository not found. If it is private, add an access token | The repository does not exist, or it is private |
+| GitHub rejected the access token | The token is wrong or expired |
+| GitHub refused the request (rate limit…) | Unauthenticated requests are limited per hour; add a token |
+| Do not put credentials in the URL | Remove the token from the URL and use the token field |
+| The folder “…” does not exist on branch … | The subdirectory is not on that branch |
+
+## A ZIP import is rejected
+
+The file is not a ZIP archive, is damaged, has more than 100,000 entries, or expands to more than 2 GB. Entries with unsafe names and symbolic links are skipped rather than failing the whole import; the import screen lists them.
+
+## “Indexing was interrupted. Re-index to continue.”
+
+The engine was stopped during indexing. Open the repository and choose the refresh button.
+
+## “The graph store was upgraded. Re-index this repository.”
+
+The database layout changed between versions. Re-index; nothing else is needed.
+
+## Tools fail with “spawn … ENOENT” on Windows
+
+Some tools start a shell through the `ComSpec` environment variable. If it does not point at `C:\Windows\System32\cmd.exe`, they fail. The scripts in this repository (`npm run build`, `npm run dev`, `npm run test:e2e`) do not use a shell for this reason, but other tools may. Check the variable in *System Properties → Environment Variables*.
+
+## Starting over
+
+Settings → Privacy → *Delete all VECTORA data*, or stop the engine and delete the data folder (`.vectora-data` by default).
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
new file mode 100644
index 0000000..822e0ae
--- /dev/null
+++ b/docs/USER_GUIDE.md
@@ -0,0 +1,172 @@
+# User guide
+
+## Starting VECTORA
+
+```bash
+npm install
+```
+
+```bash
+npm run build
+```
+
+```bash
+npm start
+```
+
+Open . The engine and the application run on your machine; nothing else is needed.
+
+## Importing a repository
+
+Choose **Explore a repository**. There are three ways to import, and a sample to try first.
+
+### The sample
+
+Under *Your repositories*, choose **Index the sample repository**. “Orbit Notes” is a small multi-language repository bundled with VECTORA for onboarding and tests. It is not a real product.
+
+### GitHub repository
+
+1. Paste the URL (`https://github.com/owner/name`, an SSH address, or `owner/name`). A URL that points at a branch or folder is understood.
+2. Choose **Check repository**. VECTORA shows the repository, its size and its branches.
+3. Pick a branch and, if you only want part of the repository, a subdirectory.
+4. Choose **Download and index**.
+
+For a private repository, open *Private repository* and paste an access token with read access to repository contents. Tick *Store this token* to keep it, encrypted, for later imports. Never put a token in the URL; VECTORA refuses URLs that contain credentials.
+
+### ZIP archive
+
+Drop the archive or choose a file (up to 512 MB). VECTORA checks it first and shows the number of files, the folders that look like project roots, and anything it will skip as unsafe. Pick the project root and choose **Extract and index**. Ignored folders such as `node_modules`, binary files and symbolic links are not extracted.
+
+### Local folder
+
+Type the path or choose **Browse**, then **Index this folder**. The folder is read in place: nothing is copied or changed. `.gitignore` files and the ignore patterns from Settings are respected.
+
+### While indexing
+
+Progress is shown in stages: acquiring source, discovering files, analysing syntax, resolving relationships, tracing execution flows, organising subsystems and concepts, generating the graph, saving the workspace. The workspace opens when indexing finishes.
+
+## The workspace
+
+The header shows the repository name, branch, indexing status, when it was last indexed, and the number of symbols and relationships. The buttons on the right open search, re-index the repository, open exports, switch the theme and show or hide the inspector.
+
+### Overview
+
+Description, languages with coverage, frameworks, an architecture summary in which every statement lists its evidence, entry points, subsystems, concepts, the largest modules and dependency statistics. Names are links into the graph.
+
+### Graph Explorer
+
+A two-dimensional map of the repository.
+
+- Drag to pan, scroll to zoom, **Fit** to see everything.
+- Click a node to select it: its neighbours stay lit, the rest dims, and the inspector opens.
+- Ctrl-click (or Shift-click) adds to the selection.
+- Double-click a file to unfold its symbols. Double-click any other node to unfold the files around it.
+- Drag a node to rearrange. **Rearrange** computes a new layout; **Group by** arranges by subsystem or by community.
+- The minimap in the corner shows where you are; click it to jump.
+- **Filters and legend** hides node types and relationship types and explains the glyphs and line styles.
+- **PNG** saves the current view.
+
+Keyboard: arrows pan, `+` and `-` zoom, `0` fits, `N` and `P` step through nodes, Enter selects.
+
+### 3D Atlas
+
+The same graph in space: the repository in the centre, subsystems around it, files and symbols inside their subsystem, concepts above, external packages below.
+
+- Drag to orbit, right-drag to pan, scroll to zoom.
+- Click a node to select it and fly to it. Double-click to unfold.
+- The breadcrumb at the bottom shows where the selection sits; each part is a link.
+- **Reset camera** returns to the starting view. **Focus subsystem** flies to the subsystem of the selection. **Show connected neighbourhood** shows only the selection and what it connects to. **Global view** clears focus and selection.
+
+If your browser has no WebGL, the Atlas says so and offers the 2D Graph Explorer.
+
+### Mind Map
+
+The repository as a tree: subsystems, folders, files, symbols, and a branch of concepts.
+
+- Use the number and arrow on a node to expand or collapse it. Double-clicking does the same.
+- Click a node to select it; the inspector shows it.
+- Drag nodes to reorganise. **Reset layout** undoes this.
+- **Find in the mind map** highlights matches; Enter focuses the first.
+- **Show relationship** draws one relationship type, such as *imports* or *calls*, between the visible nodes.
+- **SVG** and **PNG** export the whole map.
+
+### Selection is shared
+
+Whatever you select in one view is selected in the others. Select a function in the Graph Explorer, open the 3D Atlas, and the camera goes to the same function.
+
+### Inspector
+
+For the selected node: type, path and lines, description, the original source, how it was found and with what confidence, its parent, what it depends on, what depends on it, and related concepts. Open the arrow beside a relationship to see the line of code that proves it.
+
+Buttons: **Focus in graph**, **Expand relationships**, **Open source location**, **Copy path**, **Copy symbol name**, **Copy dependency information**. Drag the left edge to resize; the header button hides it.
+
+### Verified and derived
+
+Structure read directly from source is *verified*. Interpretations — concept groups, data-access relationships found by pattern, flows — are *derived*, drawn dashed or thin, and tagged “derived”. References that could not be tied to a definition are not drawn at all; they are listed under Dependency Explorer → Unresolved references.
+
+### Dependency Explorer
+
+- **External packages**: where each is declared and how many files import it; packages imported but not declared are flagged.
+- **Between subsystems**: which subsystem depends on which, with counts and an example.
+- **Circular imports**: groups of files that import each other.
+- **Execution flows**: chains from an endpoint or entry point to what it calls.
+- **Unresolved references**: by reason, with samples.
+
+**Show in graph** highlights the items in the graph views.
+
+### Search
+
+Press **Ctrl + K** (Cmd + K on macOS) anywhere in a workspace. Type part of a name, a path, a concept or a phrase from documentation. Arrow keys move, Enter opens the result and highlights it in the graph, Escape closes.
+
+The Search page offers the same search with filters by type, and **Ask about the repository**, where you can ask questions such as:
+
+- What calls verifyToken?
+- Which modules depend on NoteRepository?
+- What could be affected if I change NoteService?
+- Trace GET /notes
+- Show the complete database access path
+- Identify circular dependencies
+- Explain the application startup sequence
+- Explain authentication
+
+Answers are lists of statements. Each cites files and lines you can open, and a section called *Not established* says what the graph cannot tell you. If nothing matches, VECTORA says so rather than guessing.
+
+### Documentation
+
+Documentation files found in the repository, and two documents generated from the graph: *Architecture* and *Repository analysis*.
+
+## Exporting
+
+Choose the download button in the header.
+
+| Export | Format |
+|---|---|
+| Graph | JSON, GraphML |
+| Repository analysis | Markdown |
+| Architecture documentation | Markdown |
+| Mind map | SVG, PNG (from the Mind Map toolbar) |
+| Graph Explorer, 3D Atlas | PNG (from their toolbars) |
+
+Graph exports keep every node id and relationship. They contain names, paths and line numbers but no lines of code, unless you tick *Include source text in graph exports*.
+
+## Saved workspaces
+
+Your selection, unfolded files, filters, graph layout, mind-map arrangement and current view are saved automatically and restored when you reopen the repository.
+
+## Re-indexing
+
+Choose the refresh button in the header. Only changed files, and files that depend on them, are analysed again.
+
+## Settings
+
+| Section | What you can change |
+|---|---|
+| Appearance | Light, dark or system theme |
+| Graph | Default view, node density, label density, animation intensity, focus behaviour, derived relationships, relationship visibility |
+| Performance | Indexing workers, indexing cache, rendering quality, reduced motion, file and node limits |
+| Repository | Ignore patterns, maximum file size, languages to analyse, incremental indexing |
+| Privacy | Where data is kept, stored credentials, export defaults, the optional narrative service, deleting all data |
+
+## Removing data
+
+Remove one repository from *Your repositories*. Remove everything from Settings → Privacy → **Delete all VECTORA data**. Folders you imported from your own disk are never touched.
diff --git a/docs/VISUALIZATION.md b/docs/VISUALIZATION.md
new file mode 100644
index 0000000..41a53d6
--- /dev/null
+++ b/docs/VISUALIZATION.md
@@ -0,0 +1,54 @@
+# Visualisation
+
+All three views render the same knowledge graph and share one selection. `deriveView` (`apps/web/src/lib/graphView.ts`) decides what is shown from: node density, expanded files, hidden node and relationship types, whether derived relationships are shown, focus mode, the node limit, and pinned nodes (the selection and any highlight).
+
+## Not by colour alone
+
+Every relationship and node type is distinguishable without colour:
+
+- Node types have a glyph shown before every label, a size by type, and in 3D a distinct shape (icosahedron for structure, box for files, octahedron for types, sphere for functions, cylinder for database models, cone for endpoints, dodecahedron for concepts, tetrahedron for external packages).
+- Derived relationships are drawn dashed (3D, mind map) or thin (2D) and are labelled “(derived)”; in lists they carry a dashed “derived” tag.
+- Selected relationships show their type as text along the edge.
+
+## Graph Explorer — 2D (`workspace/GraphExplorer.tsx`)
+
+Sigma.js 3 renders a Graphology multigraph with WebGL.
+
+- **Layout.** Nodes start on deterministic discs, one per subsystem (or per community, chosen with *Group by*). ForceAtlas2 then runs in a Web Worker for a few seconds, followed by a no-overlap pass. With reduced motion the layout is computed in one step instead of settling on screen. Positions are saved with the session and restored.
+- **Selection.** Selecting a node highlights it and its neighbours, dims everything else by blending towards the background, labels the connecting relationships, and opens the inspector. Ctrl/Shift-click adds to the selection.
+- **Labels.** Labels appear by zoom level and density (`labelRenderedSizeThreshold`, `labelDensity`); neighbours of a selection are always labelled when there are few.
+- **Expansion.** Double-click a file to unfold its symbols; double-click another node to unfold the files around it. New nodes appear next to their parent.
+- **Navigation.** Pan, zoom, fit to view, a minimap with the current viewport, node dragging, focus mode, and keyboard control (arrows, plus/minus, 0, N/P to step through nodes, Enter to select).
+- **Large graphs.** Edges and labels are hidden while moving above 5,000 elements; the node limit and progressive loading keep the element count bounded.
+- **Export.** PNG of the current view.
+
+## 3D Atlas (`workspace/Atlas.tsx`)
+
+Three.js through React Three Fiber, with a real perspective camera and orbit controls.
+
+- **Spatial design** (`packages/graph-layout`). The repository sits at the origin. Subsystems are anchors on a ring (up to seven) or a sphere around it. Files and folders are pulled towards their subsystem by a 3D force simulation (`d3-force-3d`) that also links files that import each other. Symbols orbit their file on a small shell; members orbit their class. Concepts form a layer above and external packages a layer below. The layout runs in a Web Worker.
+- **Rendering.** One `InstancedMesh` per shape family; relationships are two `LineSegments` objects with buffer geometry (verified solid, derived dashed) and per-vertex colour. There is no React component per node or edge. Lighting is one ambient and one directional light with light fog for depth. No bloom, particles or decorative effects.
+- **Interaction.** Click to inspect, double-click to unfold, hover for a preview label, orbit, pan, zoom. Selecting a node flies the camera to it over about 0.8 s with eased motion; with reduced motion the camera jumps. A breadcrumb shows repository › subsystem › file › symbol and each part is clickable. Buttons: reset camera, focus subsystem, show connected neighbourhood, global view.
+- **Level of detail.** From far away only cross-file relationships are drawn (a draw range over a pre-sorted buffer). Labels are plain elements in one overlay, positioned by projecting their node each rendered frame; besides structural labels and the selection, only the nearest nodes in view get a label, up to a budget set by label density. Geometry detail, antialiasing and pixel ratio follow *Rendering quality*.
+- **Idle.** The canvas renders on demand (`frameloop="demand"`): nothing is drawn while the scene is still.
+- **Robustness.** While a new layout is computed the previous scene stays on screen. If WebGL is unavailable, or the context is lost, the Atlas explains this and offers the 2D Graph Explorer and, for a lost context, a restart.
+- **Export.** PNG of the current view.
+
+## Mind Map (`workspace/MindMap.tsx`)
+
+React Flow with a dagre left-to-right layout.
+
+- **Hierarchy.** Built from the graph: repository → subsystems → folders → files → symbols → members, plus a *Concepts* branch whose children are concept groups and their member files. Folders that contain only one folder are merged.
+- **Expand and collapse** per node; everything below the second level starts collapsed. Opening a branch pans to keep it and its children in view.
+- **Reorganise** by dragging nodes; positions and collapsed state are saved with the session. *Reset layout* returns to the computed layout.
+- **Find** highlights matches and focuses the first one. Selecting elsewhere and opening the Mind Map reveals the selection in the tree.
+- **Show relationship** overlays one relationship type between the visible nodes, lifting each end to its nearest visible ancestor and counting how many relationships each line stands for.
+- **Export.** SVG and PNG of the whole map.
+
+## Inspector (`components/Inspector.tsx`)
+
+A resizable, collapsible panel beside the view. It shows name, type, path and line range, description, source preview (CodeMirror 6, read-only, with the symbol's lines marked), how the node was found and with what confidence, parent module, outgoing and incoming relationships grouped by type (each expandable to its evidence line and location), contained symbols, member files of a concept or subsystem, and related concepts. Actions: focus in graph, expand relationships, open source location, copy path, copy symbol name, copy dependency information.
+
+## Performance notes
+
+See [PERFORMANCE.md](PERFORMANCE.md) for measurements and the list of optimisations.
diff --git a/docs/benchmarks/latest.json b/docs/benchmarks/latest.json
new file mode 100644
index 0000000..6b827c2
--- /dev/null
+++ b/docs/benchmarks/latest.json
@@ -0,0 +1,141 @@
+{
+ "generatedAt": "2026-11-12T12:20:02.515Z",
+ "machine": {
+ "cpu": "12th Gen Intel(R) Core(TM) i5-12450H",
+ "logicalCores": 12,
+ "memoryGb": 7.7,
+ "platform": "win32 10.0.26200",
+ "node": "v24.14.1"
+ },
+ "settings": {
+ "workers": 4
+ },
+ "indexing": [
+ {
+ "files": 1000,
+ "nodes": 7948,
+ "symbols": 6700,
+ "relationships": 26949,
+ "unresolved": 0,
+ "validationErrors": 0,
+ "fullIndex": {
+ "ms": 5607,
+ "peakRssMb": 488,
+ "cpuPercent": 8.8
+ },
+ "reindexNoChange": {
+ "ms": 1073,
+ "detectedUnchanged": true
+ },
+ "reindexOneFile": {
+ "ms": 5737,
+ "analyzedFiles": 9,
+ "reusedFiles": 1007
+ },
+ "saveGraphMs": 819,
+ "loadGraphMs": 165,
+ "databaseMb": 29
+ },
+ {
+ "files": 5000,
+ "nodes": 39033,
+ "symbols": 33500,
+ "relationships": 134053,
+ "unresolved": 0,
+ "validationErrors": 0,
+ "fullIndex": {
+ "ms": 22260,
+ "peakRssMb": 706,
+ "cpuPercent": 6.1
+ },
+ "reindexNoChange": {
+ "ms": 5078,
+ "detectedUnchanged": true
+ },
+ "reindexOneFile": {
+ "ms": 17485,
+ "analyzedFiles": 9,
+ "reusedFiles": 5026
+ },
+ "saveGraphMs": 4439,
+ "loadGraphMs": 1281,
+ "databaseMb": 149.9
+ },
+ {
+ "files": 10000,
+ "nodes": 77758,
+ "symbols": 67000,
+ "relationships": 267793,
+ "unresolved": 0,
+ "validationErrors": 0,
+ "fullIndex": {
+ "ms": 31307,
+ "peakRssMb": 995,
+ "cpuPercent": 6.8
+ },
+ "reindexNoChange": {
+ "ms": 10615,
+ "detectedUnchanged": true
+ },
+ "reindexOneFile": {
+ "ms": 27226,
+ "analyzedFiles": 9,
+ "reusedFiles": 10041
+ },
+ "saveGraphMs": 8899,
+ "loadGraphMs": 2541,
+ "databaseMb": 303.6
+ }
+ ],
+ "largeGraph": {
+ "nodes": 52521,
+ "symbols": 50000,
+ "relationships": 100000,
+ "validate": {
+ "ms": 539,
+ "peakRssMb": 559
+ },
+ "index": {
+ "ms": 174,
+ "peakRssMb": 561
+ },
+ "communities": {
+ "ms": 857,
+ "peakRssMb": 593
+ },
+ "cycles": {
+ "ms": 121,
+ "peakRssMb": 581
+ },
+ "searchIndex": {
+ "ms": 97,
+ "peakRssMb": 573
+ },
+ "search50": {
+ "ms": 2825,
+ "peakRssMb": 576
+ },
+ "layout3d": {
+ "ms": 3947,
+ "peakRssMb": 579
+ },
+ "save": {
+ "ms": 8043,
+ "peakRssMb": 578
+ },
+ "load": {
+ "ms": 1264,
+ "peakRssMb": 584
+ },
+ "loadStructure": {
+ "ms": 676,
+ "peakRssMb": 591
+ },
+ "databaseMb": 79.1,
+ "exportJson": {
+ "ms": 158,
+ "peakRssMb": 638
+ },
+ "exportJsonMb": 57.7
+ }
+}
diff --git a/docs/screenshots/atlas-focus.png b/docs/screenshots/atlas-focus.png
new file mode 100644
index 0000000..8376cfa
Binary files /dev/null and b/docs/screenshots/atlas-focus.png differ
diff --git a/docs/screenshots/atlas.png b/docs/screenshots/atlas.png
new file mode 100644
index 0000000..8b709df
Binary files /dev/null and b/docs/screenshots/atlas.png differ
diff --git a/docs/screenshots/dependency-explorer.png b/docs/screenshots/dependency-explorer.png
new file mode 100644
index 0000000..db023a8
Binary files /dev/null and b/docs/screenshots/dependency-explorer.png differ
diff --git a/docs/screenshots/graph-explorer.png b/docs/screenshots/graph-explorer.png
new file mode 100644
index 0000000..6221346
Binary files /dev/null and b/docs/screenshots/graph-explorer.png differ
diff --git a/docs/screenshots/landing.png b/docs/screenshots/landing.png
new file mode 100644
index 0000000..62bb3a8
Binary files /dev/null and b/docs/screenshots/landing.png differ
diff --git a/docs/screenshots/mind-map.png b/docs/screenshots/mind-map.png
new file mode 100644
index 0000000..6f36572
Binary files /dev/null and b/docs/screenshots/mind-map.png differ
diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png
new file mode 100644
index 0000000..6fa3e1e
Binary files /dev/null and b/docs/screenshots/overview.png differ
diff --git a/docs/screenshots/questions.png b/docs/screenshots/questions.png
new file mode 100644
index 0000000..bb9b74b
Binary files /dev/null and b/docs/screenshots/questions.png differ
diff --git a/docs/screenshots/tour.gif b/docs/screenshots/tour.gif
new file mode 100644
index 0000000..4127eca
Binary files /dev/null and b/docs/screenshots/tour.gif differ
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..ec63f3b
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,43 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import reactHooks from 'eslint-plugin-react-hooks';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ {
+ ignores: [
+ '**/dist/**',
+ 'node_modules/**',
+ 'coverage/**',
+ 'samples/**',
+ '.vectora-data/**',
+ 'test-results/**',
+ 'playwright-report/**',
+ '.bench-tmp/**',
+ ],
+ },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node },
+ },
+ rules: {
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
+ '@typescript-eslint/no-explicit-any': 'warn',
+ 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
+ },
+ },
+ {
+ files: ['apps/web/**/*.{ts,tsx}'],
+ plugins: { 'react-hooks': reactHooks },
+ rules: {
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+ },
+ },
+ {
+ files: ['scripts/**', 'tests/**', '**/*.test.ts', '**/*.test.tsx'],
+ rules: { 'no-console': 'off' },
+ },
+);
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..6e92037
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,11273 @@
+{
+ "name": "vectora",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "vectora",
+ "version": "0.1.0",
+ "license": "MIT",
+ "workspaces": [
+ "packages/*",
+ "apps/*"
+ ],
+ "devDependencies": {
+ "@eslint/js": "^10.0.0",
+ "@playwright/test": "^1.65.0",
+ "@testing-library/jest-dom": "^6.6.0",
+ "@testing-library/react": "^16.3.3",
+ "@types/node": "^24.0.0",
+ "@vitest/coverage-v8": "^5.1.0",
+ "esbuild": "^0.28.0",
+ "eslint": "^10.14.0",
+ "eslint-plugin-react-hooks": "^7.0.0",
+ "globals": "^16.0.0",
+ "jsdom": "^31.0.0",
+ "prettier": "^3.10.0",
+ "tsx": "^4.23.15",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.72.2",
+ "vitest": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=22.13.0"
+ }
+ },
+ "apps/local-engine": {
+ "name": "@vectora/local-engine",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/cors": "^11.4.1",
+ "@fastify/multipart": "^10.1.8",
+ "@fastify/static": "^10.1.10",
+ "@vectora/graph-engine": "*",
+ "@vectora/graph-schema": "*",
+ "@vectora/language-adapters": "*",
+ "@vectora/repository-indexer": "*",
+ "@vectora/search": "*",
+ "@vectora/shared": "*",
+ "fastify": "^5.12.8",
+ "ignore": "^7.0.14",
+ "octokit": "^5.0.5",
+ "yauzl": "^3.4.0",
+ "zod": "^4.6.7"
+ },
+ "devDependencies": {
+ "@types/yauzl": "^2.10.3",
+ "fflate": "^0.8.3"
+ }
+ },
+ "apps/local-engine/node_modules/ignore": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "apps/web": {
+ "name": "@vectora/web",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/lang-cpp": "^6.0.0",
+ "@codemirror/lang-go": "^6.0.0",
+ "@codemirror/lang-java": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/lang-json": "^6.0.0",
+ "@codemirror/lang-markdown": "^6.0.0",
+ "@codemirror/lang-python": "^6.0.0",
+ "@codemirror/lang-rust": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@dagrejs/dagre": "^3.1.3",
+ "@fontsource/ibm-plex-mono": "^5.0.0",
+ "@fontsource/shippori-mincho": "^5.0.0",
+ "@fontsource/zen-kaku-gothic-new": "^5.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@react-three/drei": "^10.7.9",
+ "@react-three/fiber": "^9.8.2",
+ "@tanstack/react-query": "^5.107.1",
+ "@vectora/graph-layout": "*",
+ "@vectora/graph-schema": "*",
+ "@vectora/search": "*",
+ "@vectora/shared": "*",
+ "@xyflow/react": "^12.12.1",
+ "cmdk": "^1.1.1",
+ "codemirror": "^6.0.2",
+ "graphology": "^0.26.0",
+ "graphology-layout-forceatlas2": "^0.10.1",
+ "graphology-layout-noverlap": "^0.4.2",
+ "html-to-image": "^1.11.13",
+ "lucide-react": "^1.72.0",
+ "radix-ui": "^1.4.0",
+ "react": "^19.3.1",
+ "react-dom": "^19.3.1",
+ "react-markdown": "^10.0.0",
+ "react-router": "^8.5.1",
+ "sigma": "^3.0.3",
+ "three": "^0.187.0",
+ "zustand": "^5.1.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.3.5",
+ "@types/react": "^19.0.0",
+ "@types/react-dom": "^19.0.0",
+ "@types/three": "^0.187.0",
+ "@vitejs/plugin-react": "^6.3.0",
+ "tailwindcss": "^4.3.5",
+ "vite": "^8.3.8"
+ }
+ },
+ "apps/web/node_modules/@vitejs/plugin-react": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.3.0.tgz",
+ "integrity": "sha512-H9E955aVvD2F4zNHCw4eIZbIfF6xKldlh7MNu+hSU7xwQzh65lI/N+CJWqmfhi0PEjDXIt/H7T7J/4wU63KJiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0 || ^0.3.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": ">=0.152.0 <0.157.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "apps/web/node_modules/sigma": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sigma/-/sigma-3.0.3.tgz",
+ "integrity": "sha512-5H0zFlx6/NTQpqBg4Rm569ZOpnBOXMaS25UQThIWMU3XyzI5AhmorK/gnl87BvJBLhQd0tW4C0LIp3enWzMoNw==",
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0",
+ "graphology-utils": "^2.5.2"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
+ "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.13",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.13.tgz",
+ "integrity": "sha512-j1nvOQCZYSgp7HLmisnCXFTpVvFODiijQ9QHkORfGKWH6YDtskTEeCJS7eV545GZ9UBzrq0e2xsOd5QQ5PjIqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.13",
+ "@babel/helper-compilation-targets": "^7.29.13",
+ "@babel/helper-module-transforms": "^7.29.13",
+ "@babel/helpers": "^7.29.13",
+ "@babel/parser": "^7.29.13",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.13",
+ "@babel/types": "^7.29.11",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.14.tgz",
+ "integrity": "sha512-qSY56jgW5JnSXzuekLVil6zaOQxOCtOHidr65KkaMBw772IuAY28Qx4ZykAcDn8+FRF15qk80XzKwgSc/RYW6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.14",
+ "@babel/types": "^7.29.14",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.13",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.13.tgz",
+ "integrity": "sha512-8WxJ4/Vmes3ghacj8VOJy3NW8eyHiFY/4thU/LSBuBE470+igjR/j1ZTNn8fV5VdNrLx6iXUwig526UdOvfP0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.14.tgz",
+ "integrity": "sha512-2XDvQVd9JESaOgGc4jT2/jquTAO7ppA5noTZQqH+MmZ89yjCzScLwtCPbWtkPZylL+TLWR49IwWmk+QkRR4GsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.14"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.14.tgz",
+ "integrity": "sha512-CXvKMFvM80ihI7Vox9jb+kQjnyjESQNPM+vB7U0Zz6TWuZA+Z/FL0QPMH300h33qJtIx+REeBhXqWOPW48TcAQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.14.tgz",
+ "integrity": "sha512-6E3Mi1r0B0umq6vmZa4/78q+64fqO2YY0THUdjDPKUCKhNb0yQy5NQIaPHCSD3ryTgiTss//P4JsWS80AsZ6LQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.14"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.14.tgz",
+ "integrity": "sha512-VL220vXuyO2z3rNIQwY7ftj9UwpqOt+33sH+sBWkOekbsSR2g7EEMnOx24ZlCH0xOA3DM/33SifVFGbnJD8r/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.14"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.14.tgz",
+ "integrity": "sha512-Y5tqevQaUAZhhj82BpYMNC1ssvLQTVAfwKdm8iy8qwmhvC2/CpcAu8OC+QEtjM7pMk7knx83u1Qj8gzkicyelw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.14.tgz",
+ "integrity": "sha512-RAdQJrBgmIH1FWvN0X+1hoDxzMzlP4cv/1Zi4dn4tG1H0cTb+rw7CzUxQJhp1f4ANd8j5vH3fy8BAWFGkBDEsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.14",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.14",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.14",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.14",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.14.tgz",
+ "integrity": "sha512-UjI/dKmwJwT9pOszJitq2G6EObZPnsZUfdCr92z2TlF3EZK9J3EdfOKUrCzHl6wFthbzwhzl1Soc/UIwxRiq6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.14",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@cacheable/memory": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
+ "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/utils": "^2.5.0",
+ "@keyv/bigmap": "^1.3.1",
+ "hookified": "^1.15.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@cacheable/utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
+ "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.5.1",
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.20.4",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.4.tgz",
+ "integrity": "sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.11.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz",
+ "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.7.0",
+ "@codemirror/view": "^6.27.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/lang-cpp": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
+ "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/cpp": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-css": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
+ "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.2",
+ "@lezer/css": "^1.1.7"
+ }
+ },
+ "node_modules/@codemirror/lang-go": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
+ "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/go": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-html": {
+ "version": "6.4.12",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz",
+ "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/lang-css": "^6.0.0",
+ "@codemirror/lang-javascript": "^6.0.0",
+ "@codemirror/language": "^6.4.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/css": "^1.1.0",
+ "@lezer/html": "^1.3.12"
+ }
+ },
+ "node_modules/@codemirror/lang-java": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
+ "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/java": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-javascript": {
+ "version": "6.2.5",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz",
+ "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-json": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
+ "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/json": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-markdown": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz",
+ "integrity": "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.7.1",
+ "@codemirror/lang-html": "^6.0.0",
+ "@codemirror/language": "^6.3.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.2.1",
+ "@lezer/markdown": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-python": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
+ "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.3.2",
+ "@codemirror/language": "^6.8.0",
+ "@codemirror/state": "^6.0.0",
+ "@lezer/common": "^1.2.1",
+ "@lezer/python": "^1.1.4"
+ }
+ },
+ "node_modules/@codemirror/lang-rust": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
+ "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@lezer/rust": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.13.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.13.1.tgz",
+ "integrity": "sha512-zfvU2r3TyylQLMj/te+5CnQ0rSqZi8B1oTCBbQkagcLW4Xc6imab6qN+pEJhsuNOLaJqiRyBXp9UhWA1DhVvDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/streamparser": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
+ "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.42.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz",
+ "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.37.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.7.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz",
+ "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@marijn/find-cluster-break": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/streamparser": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/streamparser/-/streamparser-6.0.0.tgz",
+ "integrity": "sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==",
+ "license": "MIT"
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.43.15",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.15.tgz",
+ "integrity": "sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.7.0",
+ "crelt": "^1.0.6",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@dagrejs/dagre": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.1.3.tgz",
+ "integrity": "sha512-X4aUTwOReZvthaZWOVHqlJ52/ZdhZFEM4+Lh/uR4XooUSLE6p71bcy7MPGzYnNUcNh+nj+i/qJQpCMIdQnSmrg==",
+ "license": "MIT",
+ "dependencies": {
+ "@dagrejs/graphlib": "4.0.6"
+ }
+ },
+ "node_modules/@dagrejs/graphlib": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.6.tgz",
+ "integrity": "sha512-Hjv7hbfmSBMP4nVXMFt4gusqWWHghQF0qNT4V5KHo4Jatvd+2L5VHKFnnIjXVuDnaHIRz7ZuHSnlf0RuZkOwWA==",
+ "license": "MIT"
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@emnapi/core": {
+ "version": "2.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-beta.2.tgz",
+ "integrity": "sha512-MzCVoWtcg4Pm2B3je5mMvYMvj3MmBKw+fU7vbsJc9/iT8ip6/eYIHo/Dsm/mr+wsHB6SEyJDuxB09Yy6r/8+4A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "2.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "2.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-beta.2.tgz",
+ "integrity": "sha512-JcrSPzPvZuNZvJqPTF9Fw25LbOJ8avzFVlaf2grmH4Cku0oGP47tRgqPZadcfj3YoZMHkFVnCYFbQM8I+i5QEw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.2.0.tgz",
+ "integrity": "sha512-JP7OYH38Q7+pyTAe14raZHLQIK7BDKbdwji+Xi3pcd/6QkJYQH3SX4wu6EplGrGJWRepoNzedfm0G3B9Z4e9Rw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.3.tgz",
+ "integrity": "sha512-R76UKbvsqI68DJJVTVyqFKqU98CN4+B+acxNfEA2IFarSzP0SjtKf/3+fFiXzTyv5Fj3PxLkGHe2wnVWWMxobQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+ "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
+ "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@exodus/bytes": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz",
+ "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@fastify/accept-negotiator": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.2.tgz",
+ "integrity": "sha512-LV7MZqTwXTEt3GxBT6BU8ACztWnv++hSkMEsP3usIHiI0tA4F5EO4THhDCOr5J/ginM9DCnLCmZ6u3JLA9x8Iw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/ajv-compiler": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.7.tgz",
+ "integrity": "sha512-VP0yIfwlc1ohj733keYDqyHm1VWeP9VQxYomunhr90jkC4aNNibbkrRiaNgPgqj1ORxUenILR4qzA/ppYBNAtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^4.0.0"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.5.tgz",
+ "integrity": "sha512-y8MTAeBlEQUNVnskmZQwpseZN3EgCksQNOULlfxWY6o6VEqh01+oExMgbM7gN6YLQ8pNDYeweNPKJRt8jQm2ZQ==",
+ "license": "MIT"
+ },
+ "node_modules/@fastify/cors": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.4.1.tgz",
+ "integrity": "sha512-WuPATGGyc3YzpOfumovBJ4xC2RPBBFyDFElwldFRBCgM2G27xH9DnXTho9+nc2Ho6gLD1hKTetdOntR+G38+7A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fastify-plugin": "^6.0.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/@fastify/deepmerge": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.4.tgz",
+ "integrity": "sha512-kc8GQq5gX9RRhw0rov3DMrN8wS8yRlxJRdDzsYwwdBV/wazuVgc/1K7CKL90tlvu3RlHan6Y0R0aE3nJtBz2eQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/error": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.1.tgz",
+ "integrity": "sha512-MYBumuIwP+5MIpXSyKm6/o1Eigcoij186lWUssX3LH5U8KWqU0ktZhzC4CErIX9EJeQeRGI+zJZQWakBlFbUcg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/fast-json-stringify-compiler": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.1.tgz",
+ "integrity": "sha512-pyxRKk7UpUnnrztP9pF4+ZYxusEQ4LifTyzRdiXmyql1QezNHxO02D25MVU2o4kMeAMYWAjG6RKRlojgkbiDAw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "fast-json-stringify": "^7.0.0"
+ }
+ },
+ "node_modules/@fastify/forwarded": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.1.2.tgz",
+ "integrity": "sha512-89WiRG+hW3VQSfCwg4Hb8b43PeyEX15K5+lEfsZ4EUth/FyOjS/lv8HsVuswUNXWiSnxMGNouE/fq/0w3VumHQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/@fastify/merge-json-schemas": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.2.tgz",
+ "integrity": "sha512-d/gx9sRbUCVgGRGzma46wLcwiY8noTqO2Nmjg/vbTARxHRE5bdh5PIL8s97Lt1X7ScRWRW52eVYxwIoPwIdVUA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/@fastify/multipart": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-10.1.8.tgz",
+ "integrity": "sha512-GNZ6bKyqiKpb1VRhOJtb38X92NSEQIM+piO3sid13oP5xmuVmdbFq32khGh4Ru/jMAJoAVxt4yzuUNPwc/CdSg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/busboy": "^3.0.0",
+ "@fastify/deepmerge": "^3.0.0",
+ "@fastify/error": "^4.0.0",
+ "fastify-plugin": "^6.0.0",
+ "secure-json-parse": "^4.0.0"
+ }
+ },
+ "node_modules/@fastify/proxy-addr": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.2.tgz",
+ "integrity": "sha512-uqB4QsgVoro0XDC+DIftIMGXHdBPn+lK6Ek3xXAIxPMCJfqxnbtyEoMCm2KPUVOuIosd7Cyw9HawEqMFM/UA+g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/forwarded": "^3.0.0",
+ "ipaddr.js": "^2.1.0"
+ }
+ },
+ "node_modules/@fastify/send": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.2.1.tgz",
+ "integrity": "sha512-zOB8L9z444PnGbd1Nd7gNXPz/7bnDR8Z4KNTNSwLVQuE3cTGsU9qUj1N4SjXKK0Tq4cauZYTwB+tqvlqosuKsw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@lukeed/ms": "^2.0.2",
+ "escape-html": "~1.0.3",
+ "fast-decode-uri-component": "^1.0.1",
+ "http-errors": "^2.0.0",
+ "mime": "^3"
+ }
+ },
+ "node_modules/@fastify/static": {
+ "version": "10.1.10",
+ "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.10.tgz",
+ "integrity": "sha512-CGwOoZiKg5m4SAqeiS8bzCSKDa71DMFQ7QeVsuWBOrfFu32DpVZWFFb0cpEg7e54YK6HZZsl4g/wfoKMnRQMUw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/accept-negotiator": "^2.0.0",
+ "@fastify/error": "^4.0.0",
+ "@fastify/send": "^4.2.0",
+ "content-disposition": "^3.0.0",
+ "fastify-plugin": "^6.0.0",
+ "fastq": "^1.17.1",
+ "glob": "^14.0.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
+ "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.8.0",
+ "@floating-ui/utils": "^0.2.12"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz",
+ "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.8.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.12",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
+ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
+ "license": "MIT"
+ },
+ "node_modules/@fontsource/ibm-plex-mono": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/ibm-plex-mono/-/ibm-plex-mono-5.3.0.tgz",
+ "integrity": "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/shippori-mincho": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/shippori-mincho/-/shippori-mincho-5.3.0.tgz",
+ "integrity": "sha512-S41kRxQuubAaYss+LeXu0IOc02RQwsPaJbR1cWZx4VV15hXaYZViH3WAve9HpDVFqKYd/BVk86Bg7lp9ytV3KQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/zen-kaku-gothic-new": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@fontsource/zen-kaku-gothic-new/-/zen-kaku-gothic-new-5.3.1.tgz",
+ "integrity": "sha512-L69mE6bkzeVK3UdTenxOmCIWvTwMxohJRNm1dAj+W+UiCULQ91b7M77ENVw+pjGmFL6zKPFYFeH3g8x2ugAJNw==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/nonrecursive-glob-match": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.1.0.tgz",
+ "integrity": "sha512-GPm/Dbvk5RLmhRnxXIa7nTpK0xtl4E0wj0MslsuNiJ1FIROU+2+9lgUC/dgF7Ey4QCWhfEsgxilMRMCdHPfDfw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@keyv/bigmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+ "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hashery": "^1.4.0",
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "keyv": "^5.6.0"
+ }
+ },
+ "node_modules/@keyv/serialize": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.5.tgz",
+ "integrity": "sha512-Pw7PVCndruF1pb3ebCxpSVpkMHSDjwRCLcTRw+RizUKco5QbWylwq2g4lnG/RYN7yTTQDXE9F77MwtkAyphrjg==",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/cpp": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.6.tgz",
+ "integrity": "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/css": {
+ "version": "1.3.9",
+ "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.9.tgz",
+ "integrity": "sha512-ZmzgvTOBdJWGIsY/ItAfN7UW8DV802oZG+jeG6E508SwEzz5YhuAajptpWPICvIyqYAD2+VjMdOS40GgGkyEOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/go": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
+ "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz",
+ "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/html": {
+ "version": "1.3.13",
+ "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
+ "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/java": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.5.tgz",
+ "integrity": "sha512-JppTd1iKHpeNlLYw/Zl8JX7zSY5HqAzK4zZ05lpn8l8Sk7Fo5KAKiDiMiezKoT5lfeC2xZf8HeKp1tSctj4G6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/javascript": {
+ "version": "1.5.6",
+ "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.6.tgz",
+ "integrity": "sha512-sEvOqpl4vJQca0NugSQK02vQPbc5KrvoojnYGoqqFgFO0MtcYhwGiHKVEB+LuXYJnfyXo4QPA8ztbrEa4Fw5NA==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/json": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
+ "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.12",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.12.tgz",
+ "integrity": "sha512-On9HU4oMMiHPMkQDgoTnGahjgG5n/MkN38CgG77sMraKLL+3JBbeQNi1FcCXV9QRqwF9/BUXP3caHyJC1fBOTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/markdown": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.8.1.tgz",
+ "integrity": "sha512-M5YmEcKWO4mJ45Y1hECwqyu8cLv25EiXOK554PVOvDOWOP0olwelHHQUWr9sjF+d1bENVhy0nfJ8E57XZUTiXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.5.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/python": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz",
+ "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/rust": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.3.tgz",
+ "integrity": "sha512-XxErOjZzQ7yJt1agUT4fu9qQvESZ3acgoxpPaPTPOiUx+duCjaVAtZGFIgphkHxlN05djdVAIOy/wItShMEjqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.2.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0"
+ }
+ },
+ "node_modules/@lukeed/ms": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
+ "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@marijn/find-cluster-break": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz",
+ "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.17",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@monogrid/gainmap-js": {
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.7.3.tgz",
+ "integrity": "sha512-YjgdH65wZ9lQnzkVWerrR9FJR0JYhn3ahV7DJqDZVtIvR/kWv7XzFbMSIOFS/47+nWbZ5zFTRxsFhq0OX//SuA==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.6.tgz",
+ "integrity": "sha512-mRyDmEw9/fCDXCGfL1sbgIw/GUK0PVxRW4YwgPN61F3LQ+fSyoBFarJVzgGyhF6kRzEQZ0FejDqm4WLXSeqQmw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1 || 2.0.0-beta.2",
+ "@emnapi/runtime": "^1.7.1 || 2.0.0-beta.2"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@octokit/app": {
+ "version": "16.1.4",
+ "resolved": "https://registry.npmjs.org/@octokit/app/-/app-16.1.4.tgz",
+ "integrity": "sha512-g70WONQyGoBgqIJtv4O0MUEfOF1iJpTfFt3qAW7HaiwiJ6k607xg/gBezCa4tuq6BpO6qXn7qvq8v9tjWtgGQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-app": "^8.3.0",
+ "@octokit/auth-unauthenticated": "^7.0.4",
+ "@octokit/core": "^7.0.7",
+ "@octokit/oauth-app": "^8.0.4",
+ "@octokit/plugin-paginate-rest": "^15.0.0",
+ "@octokit/types": "^17.0.0",
+ "@octokit/webhooks": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/app/node_modules/@octokit/openapi-types": {
+ "version": "28.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz",
+ "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/app/node_modules/@octokit/plugin-paginate-rest": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-15.0.0.tgz",
+ "integrity": "sha512-lw9A9YL5s4VPj+VEx8uMxaxQm2YTgrPDkrLEuZuu18R8TK3oPcXF4K6t52nx6xn1RcogZC9i188sAr/4XYJaQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^17.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/app/node_modules/@octokit/types": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz",
+ "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^28.0.0"
+ }
+ },
+ "node_modules/@octokit/auth-app": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-8.3.1.tgz",
+ "integrity": "sha512-XHWxZsF4mvGTQCv7AOxgby9t9OjVO19FBuKq+QmyBxFrf+B2hR/P5j/MTF8g95bITf8cRtR17KIXThpFNqiKbg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^9.0.5",
+ "@octokit/auth-oauth-user": "^6.0.4",
+ "@octokit/request": "^10.0.16",
+ "@octokit/request-error": "^7.1.2",
+ "@octokit/types": "^18.0.0",
+ "toad-cache": "^3.7.0",
+ "universal-github-app-jwt": "^2.2.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-app/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.5.tgz",
+ "integrity": "sha512-Hn2yAlIDjFLGjAz7t+CRu7e9VYoFW1W3LLyOLyahFoJGlCf4PtDkHKtRET8w/2CJpsrC0FxIF5g5Xtx9E4O78w==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.5",
+ "@octokit/auth-oauth-user": "^6.0.4",
+ "@octokit/request": "^10.0.16",
+ "@octokit/types": "^18.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.5.tgz",
+ "integrity": "sha512-kdJtjRhykMLJrMdcoC56XUBDfoUAuWEFKpkDxwYim+bNwYw1MreLolk+PLnwEYT8djm589YepeMuJ8+ECAIzVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-methods": "^6.0.5",
+ "@octokit/request": "^10.0.16",
+ "@octokit/types": "^18.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.4.tgz",
+ "integrity": "sha512-eya2pMJ9pCBq1jrm1Rf6J3NI+NemEpzDMLo9J/GpRoK0KgdxIVXWX4LESIfwJ+GYeoq6QFIlUJNi1lVp5FBgmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-device": "^8.0.5",
+ "@octokit/oauth-methods": "^6.0.5",
+ "@octokit/request": "^10.0.16",
+ "@octokit/types": "^18.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/auth-token": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
+ "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.5.tgz",
+ "integrity": "sha512-KBzGMM8hRgtGKDe8JqvDdhHj+7PWHRo1qxz9pQ5WT1162DLdC6O5jL+kRFk3VfpCnEfc3bUYZ2vOwI9vx8yUXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.1.2",
+ "@octokit/types": "^18.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "7.0.8",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.8.tgz",
+ "integrity": "sha512-L7y8eYc+AwxGr2PWI4WFt1VG4TiJ66c26BD16mXpYIlXxG0SMigM1+m4aTSlYyBr5BlQsGAlz8uDCoZN4SEMcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-token": "^6.0.0",
+ "@octokit/graphql": "^9.0.5",
+ "@octokit/request": "^10.0.16",
+ "@octokit/request-error": "^7.1.2",
+ "@octokit/types": "^18.0.0",
+ "before-after-hook": "^4.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/core/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.5.tgz",
+ "integrity": "sha512-iXa654H3yFafF/ieHkukfbgWo2rmXD2ceD0ZOtrPhw1bc3FDch1d9N/TNs0FQ1/cIbwb7kspUX8jzIs8nzb9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^18.0.0",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/endpoint/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.5.tgz",
+ "integrity": "sha512-bt/hm03LeU6Vy7FwTrkkC9p3XGT/lBwClglMqxBSe5/q0E5CdJTXeAqEI0vlw89/LF/G6tryTIH8HirZ3prMVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request": "^10.0.16",
+ "@octokit/types": "^18.0.0",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/graphql/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/oauth-app": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.4.tgz",
+ "integrity": "sha512-Ji5JpRwAJcbOJkO4ij0tW6+GrQ8efpICnAca3o5xI8/HUNjqJu3hhG4cCZdXAHMKaOaQTFX0Yl+cpu61Rsx94A==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/auth-oauth-app": "^9.0.4",
+ "@octokit/auth-oauth-user": "^6.0.3",
+ "@octokit/auth-unauthenticated": "^7.0.4",
+ "@octokit/core": "^7.0.7",
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/oauth-methods": "^6.0.3",
+ "@types/aws-lambda": "^8.10.83",
+ "universal-user-agent": "^7.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-authorization-url": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz",
+ "integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.5.tgz",
+ "integrity": "sha512-/mAz7taDZD7DcV4zcG6TDw3Kr6TOldmYBRpvA62C3WIxRMmcgX8XxsTvVQomqKE7VEk/BgYhUao1aR2tYGyIKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/oauth-authorization-url": "^8.0.0",
+ "@octokit/request": "^10.0.16",
+ "@octokit/request-error": "^7.1.2",
+ "@octokit/types": "^18.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "27.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
+ "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/openapi-webhooks-types": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-webhooks-types/-/openapi-webhooks-types-12.1.0.tgz",
+ "integrity": "sha512-WiuzhOsiOvb7W3Pvmhf8d2C6qaLHXrWiLBP4nJ/4kydu+wpagV5Fkz9RfQwV2afYzv3PB+3xYgp4mAdNGjDprA==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-paginate-graphql": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-6.0.0.tgz",
+ "integrity": "sha512-crfpnIoFiBtRkvPqOyLOsw12XsveYuY2ieP6uYDosoUegBJpSVxGwut9sxUgFFcll3VTOTqpUf8yGd8x1OmAkQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
+ "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
+ "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^16.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=6"
+ }
+ },
+ "node_modules/@octokit/plugin-retry": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.1.tgz",
+ "integrity": "sha512-VCVvZ/R1+u3WuiBWpNavZ0mY4aaJNAsENrpBP9aLSR2QyOpQgd7DhM5j4AW7z4MQpnJYgwBPf0XqPQoNBRdQwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/request-error": "^7.1.1",
+ "@octokit/types": "^17.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": ">=7"
+ }
+ },
+ "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": {
+ "version": "28.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz",
+ "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz",
+ "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^28.0.0"
+ }
+ },
+ "node_modules/@octokit/plugin-throttling": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.5.tgz",
+ "integrity": "sha512-LIdrkrUv+DWbKeg/49rGuFJ3SU0d3hUS+B4MhNZLepBoNUFXms8Ic9edJjrlx+zycqJHjrMRudVpVb/bAXM2Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^17.0.0",
+ "bottleneck": "^2.15.3"
+ },
+ "engines": {
+ "node": ">= 20"
+ },
+ "peerDependencies": {
+ "@octokit/core": "^7.0.0"
+ }
+ },
+ "node_modules/@octokit/plugin-throttling/node_modules/@octokit/openapi-types": {
+ "version": "28.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-28.0.0.tgz",
+ "integrity": "sha512-0rFyLuyHvIj6uuZWuDslxkowFYdPXoNIkeAv4b27dzm2Tf4vGWXnPsMcxs7d65kLdMERgP3wc1AEPlqMz8e1cQ==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-17.0.0.tgz",
+ "integrity": "sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^28.0.0"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "10.0.16",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.16.tgz",
+ "integrity": "sha512-A0zWGjHzISIb+9ccG8s0dq7LKO5zVpJLRICjgUb+sJxEWqn8RUHB1rD3AE51+PECvXHIxqZ1VVvs4fHTSD9nUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/endpoint": "^11.0.5",
+ "@octokit/request-error": "^7.1.2",
+ "@octokit/types": "^18.0.0",
+ "content-type": "^3.0.0",
+ "json-with-bigint": "^3.5.12",
+ "universal-user-agent": "^7.0.2"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.2.tgz",
+ "integrity": "sha512-XZRuT3xZ84D3gYErI1DZvhJ33dCWVV6uzBtWkaBB4TvA/L6eOeTZodxLFVB44bBEEo3vEx7y00UfX1tBLrtLRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/types": "^18.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/request-error/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
+ "version": "29.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-29.0.1.tgz",
+ "integrity": "sha512-9qWOMFNxxLokERcms42rU0PTLqQmVs7g5E41TI4mCOxmpFayD1rfC7XxOL55cG9MBZLFlC31BrR37myMKardwg==",
+ "license": "MIT"
+ },
+ "node_modules/@octokit/request/node_modules/@octokit/types": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-18.0.0.tgz",
+ "integrity": "sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^29.0.1"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "16.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
+ "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-types": "^27.0.0"
+ }
+ },
+ "node_modules/@octokit/webhooks": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-14.2.0.tgz",
+ "integrity": "sha512-da6KbdNCV5sr1/txD896V+6W0iamFWrvVl8cHkBSPT+YlvmT3DwXa4jxZnQc+gnuTEqSWbBeoSZYTayXH9wXcw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/openapi-webhooks-types": "12.1.0",
+ "@octokit/request-error": "^7.0.0",
+ "@octokit/webhooks-methods": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@octokit/webhooks-methods": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-6.0.0.tgz",
+ "integrity": "sha512-MFlzzoDJVw/GcbfzVC1RLR36QqkTLUf79vLVO3D+xn7r0QgxnFoLZgtrzxiQErAjFUOdH6fas2KeQJ1yr/qaXQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.156.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz",
+ "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@pinojs/redact": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
+ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
+ "license": "MIT"
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.65.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz",
+ "integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.65.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz",
+ "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.2.0.tgz",
+ "integrity": "sha512-KABY+VMIRPJL+Vyzv/335FEhXXD/xDv8BMb1kl48/aXJZMH0LtHCTx5ga5RnxaEP3nZHtmTLf1P9vanmFX46bw==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.2.0.tgz",
+ "integrity": "sha512-BB4+HHkFl3HipEAhm/mLhrpXV+2kRX7MvY4AaImUcQVeWQt5KhV1JEByNs8NGfJOcr95RcQGijt9+OrCo68xGA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.3.1.tgz",
+ "integrity": "sha512-lCZ6sbGS/7WO0j84yx4srIgOf0MRJ/sZ13RonADXxvKBym8xrxNCq5f6DibGcr38sIlwPk8/52uYdXeOPsbEDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collapsible": "1.2.1",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.2.1.tgz",
+ "integrity": "sha512-MWC4N7RY+qsbNrFNo7yQ9ZbdoZEW+B5DtpWxJfUnVCqcQxxqqg3qis3RMPTDbCNaTIdHJSn5XDkl9ZdVWXknLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dialog": "1.3.1",
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.17.tgz",
+ "integrity": "sha512-67jjcVvxjXCCStnTW3Z+M7jmaFMuMR7M2JgpDzH4w7JSRrikmzhjfYgP/09ECb8n7whnsgh1ifJ+Ie96qEj+pQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.2.0.tgz",
+ "integrity": "sha512-gHEU+Lx8DlT/FYIjZQvvBJrFSwUngXTjh7FeL9QvJaNALDLez6pgDj2axCUihBjtPBy82LN0gNb25+sTcSXNmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.3.1.tgz",
+ "integrity": "sha512-ssZOkfQNkFpYVmCPQlr978xSaU7moSrGXdwg4j2Xs2WlGl/1KsSZtfpK+WnLnx0Rwll1BAh6oUrAy/uR31SBvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-is-hydrated": "0.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.4.1.tgz",
+ "integrity": "sha512-KWtwhh+98ML3seZ9vO9yRY2OwaQG90dCiDAxjpSNY2AaZ3Hik3B0dsk6m0ILywo/no3A9/SozadyPMNweVT8yw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-size": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.2.1.tgz",
+ "integrity": "sha512-fSzLb/qy1wuZSxQBcxpW0hNXYHDMWiIXDGyJGdA4+FSLknOWZsNRrTCUavxb4PI8b8lvHo/CysO6NVi5zLpo5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.2.0.tgz",
+ "integrity": "sha512-+yiQzqcc7Sy7/5s0jrMCjpPRFB3538VNrXPWdTLZhE/SCfgEws4aI6lHWN/vvcnyhHndROWBzqmqB4QBrITZ9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz",
+ "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.3.0.tgz",
+ "integrity": "sha512-v5AYEocMISkbJ+1fTXzZpiFv7NRr3D2WiYbLIKf+FkmJ8VkP6d503eV2n34MFussHXY1pcoq3yfJiQH77dc4Mg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.4.1.tgz",
+ "integrity": "sha512-Pddn3Z8lio3/ChxyXaEP3jHTTo1LTHCWwF8+mG1chQlWijP+ttVHBKwezrQTiV6Mj670ojvumqvuGh+mdv33iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-menu": "2.2.1",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.3.1.tgz",
+ "integrity": "sha512-+0E4qvVTwjAWra9sEhJ+9BTsvHE00zkUuNw1VGh7S+Gc13dfYk+S1PVQdchWDj+XYHhAai7Z7IpzXVDCGsDplg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-guards": "1.2.0",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.2.0.tgz",
+ "integrity": "sha512-FtRA52a/r+LKStFT+neLUm+8vSe7wbzkbQGGeU8ixFiwC8NU14Evoq9ATECqL3eGbcUzJR4fyfA4KkK/q6dfqg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.2.1.tgz",
+ "integrity": "sha512-KIfDcAsx+oVylFZ4xryVKusJUrLLhTQmfnJW4kwk4Km5myvhHCrj04hnWWUC166+xD4DHdH8tQGylwYX1h6+Pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-effect-event": "0.0.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.2.1.tgz",
+ "integrity": "sha512-XsHcmYG2AcqnvTqrOXXWcMrhquEIlqucizuDH1MV/qMRW+DndpBCMvNeTRXOxqmG1H5j89M/+I0/34Zdd+zDcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-menu": "2.2.1",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.2.0.tgz",
+ "integrity": "sha512-MPm5R0iRGUDrDnPNxmLBkv+X7pN4XKzt+MbWJEuJQsH3NFH4omrbpXpucyYCCKTSt+na/ZxGhEWQrWTZ0285cA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.3.1.tgz",
+ "integrity": "sha512-ugeMhbXt4y5S2T1GnIqbWDKEN962PDwnX1fN0eKOOgf5f63Dh2mn8F1FUySlgW0iF6GiVsRF32DzuDIWTUyQJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.3.0.tgz",
+ "integrity": "sha512-KnmrCM27m7Jo1R8S6cFDPrEeq20Jfn5dqde0SZrJr2lVXlj6/QZSUCyFoYh1d8ti0uUrMNK5Lx6dUDuINT+f4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-label": "2.1.17",
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.2.1.tgz",
+ "integrity": "sha512-4QooHxgmpCKRjj1Pft+hETKidYcrbPKcVwMGtk2Oa5DcWY2IH1fHuQt9LG6q5hUQBBV2MhQxSvtx6B/C1xfiTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz",
+ "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.17",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.17.tgz",
+ "integrity": "sha512-xmj7MN/drx1oUAXkfiFIvyDmGNUIN/omGobvA5oAoNqu9zO2MkISYGZOkBcqSzs/UZXXwV+/3ACRE7q7mGcmkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.2.1.tgz",
+ "integrity": "sha512-G34BsU90SBj5foYpz8pQqq/Q0Dlfyyl6KitWEfRwXC0QvfwhWNAwS49DWP2+rRJ0iwv20Z2Qx7Sk+T03bGo5JA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-guards": "1.2.0",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.2.1.tgz",
+ "integrity": "sha512-bRTYtS9JITV2dUYqYZb7+N5SooQBkZrNRejfmyrSKe3Y57aXhKgx//kBUP0kY7pEpsWR/U1AHsAJRzjwCrK3PA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-menu": "2.2.1",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.4.1.tgz",
+ "integrity": "sha512-XPPZjkeyCSMNNvt0FyDv/PUauG7Ow4GwHQ/XZSwW36F/TS7rR4HFTsXH8ewLnXTOLn3x96/pmTPNQIVoTX/86g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-use-previous": "1.1.4",
+ "@radix-ui/react-visually-hidden": "1.2.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.2.1.tgz",
+ "integrity": "sha512-KtHdcgnaAK6rohQ/MMJoGYcbgSRJsIuGBdHrFeTB1/9SaKlDs14oSOniW6BL6M43HqvgCO3u4WuVdFLkuc6sqw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.3",
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-effect-event": "0.0.5",
+ "@radix-ui/react-use-is-hydrated": "0.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.2.1.tgz",
+ "integrity": "sha512-vu4+ZoJAaFjdI7l1lDSZRs+4dEv83Jmu7lyzajPrAeQzX74y84n0rUNWbWSgN9XB5NE8IY86Z02kGH6oRrzR+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-effect-event": "0.0.5",
+ "@radix-ui/react-use-is-hydrated": "0.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.3.1.tgz",
+ "integrity": "sha512-ZNxvXRx5W2Wos4fXCXb5lpdqN0Z5chf+mStOUOTr4ik7d+62MRvoe9rzCA/eGNg36lxXqSrKEKmwuZEcUrIstw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-guards": "1.2.0",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.4.0.tgz",
+ "integrity": "sha512-yCWRvLeGv9Pz3+efwHqGPp70mZ56r3kfIgQVF56Oj2aAa4uP/Tb+IE59wNcc+758rctqbCKx4LbK8II/r7Bn0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.17",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-use-rect": "1.1.4",
+ "@radix-ui/react-use-size": "1.1.5",
+ "@radix-ui/rect": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.2.0.tgz",
+ "integrity": "sha512-StX7xXnaRU41VvFtMm9zlhVGz8XF6GmWWTXftkQaY+tqVkbs/R9UmAp5mLzHHapk5hM9RMZQvO/3usyIP/g42g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.11.tgz",
+ "integrity": "sha512-Z7/atlkab36Z8hm+V8zYc79r0W+tiOB1o0tun1By94JyadkekLQmtEAnBSP9XKXBNQsStoStS4+gX10JpS5akA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.2.0.tgz",
+ "integrity": "sha512-HCTSPMNqv55Qk0IX4geJbpyXazmqulcimwuyjxTkKi29v+QZbG2Zho9BnbBq4YS2PF+KlNb2CXoLz66uUmblkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.2.1.tgz",
+ "integrity": "sha512-yQBiM6J3YM8Ums1orkEFpbznK47geKxa0igikzcaS1xHgdba3DdAQBsGbCTt5BVfR8zhEvgxQ0poGcINaD3lvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.5.1.tgz",
+ "integrity": "sha512-QbyVT1ddnjqB9H+CUYKkNWdU9fIK3QHCYtjf7mS6ekSqiuXkWtWn5UZW9mhA8qtl3vrVyIsmR9gePstAtKrqkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-size": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.2.1.tgz",
+ "integrity": "sha512-UwraORzVUeEPMclhO1p0vz0qI+TbdDLO9i+FiOqhJyHKwdpCRCck9yVEscUnAMIzCiuNoQa8D4A9bIpOPXGt6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-is-hydrated": "0.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.4.0.tgz",
+ "integrity": "sha512-CFrLqHNejWq5FhKAcj9V/WPGRsol76HBQto81NdOBVFEOqIEkJkKIJt0Z824A/9hHLCtoiR/Kw3euRgzXP2pxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.3",
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.4.1.tgz",
+ "integrity": "sha512-888ZwW0Md6GyptuqYABRX9nLKgoYfcooCyBBvVaX5Jp3e+8Jp2i4Zo/Uy50Hm+Ysn2JfloQXstW4U36w8teVtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.3",
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-guards": "1.2.0",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-visually-hidden": "1.2.13",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.7.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.2.0.tgz",
+ "integrity": "sha512-1jUAap0wZEqVP05Ou7kJgRxe/W+Qftng+rUNWGa0qkKbv9r5pWTG3E7a3ELKo2eJoHKMECnzuYrZpSyO1YgcYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.6.1.tgz",
+ "integrity": "sha512-IRNRR90moPqxfwCJ0XbMJIxo70W0/1ZExT/sNCxEHMaPx0SF9HoQrcPEC75+H28cFKa0b+KH63anfLYJmwKacw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.3",
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-use-size": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.5.0.tgz",
+ "integrity": "sha512-tyYd9ddPXAysAbdbD9eYxgwpCSv6f3PncqVSNKQFSR5lcJVLz3cb/pANDoYrLiHBpXv7skortyUgGUWsYuPPFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.4.1.tgz",
+ "integrity": "sha512-NpBGwc40rY02ZmSvuyeKk0Bvc8ZsNeTMi4qHvJ+ZH4EiYCv0zIXEgdCW4bVdzgIUeP9xxbLJSxKOUfqITYNmkA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-size": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.2.1.tgz",
+ "integrity": "sha512-hRoJlode+hg5ZNlR+3tkfkUliIybdTpD5lkAvHQ9yaXseGf7XVO9Ib2GPEIOuaJJbo7YWYbNpDDRfo8Dhf6/xA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.3.1.tgz",
+ "integrity": "sha512-iQvL0J7XK4HtJNGkkWMyYrboNseI7vtotFeUkIno8YNxVA9oM7B6//fh0Mc6TSKI96+zioLWi98lQLso88yJmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-visually-hidden": "1.2.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.2.1.tgz",
+ "integrity": "sha512-+3MUq+WyiBplXYqQ0EIYlq8R4sLQ0T5NbOK7DMdDLCKO93pe2uE0DUJTWWfTBprpJJXxfoF3eMW0xpakVZrYsg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.2.1.tgz",
+ "integrity": "sha512-O8Ldx5ao4p4KoW+DO8/PmkOe5mdKNi3F2SseqjVj88wvDGDHe60cUmO0Zm/uHsKrvNgQGV8Efpuh95Tht/h4vw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-toggle": "1.2.1",
+ "@radix-ui/react-use-controllable-state": "1.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.2.1.tgz",
+ "integrity": "sha512-Cm8m7JTEeUdbfA6T9xgyu8HfOpyvSx8pB/wj0DlEuWaJe9FilIs4Az9wci/lU9RbthV/UkltszMw5WnnD+m/Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-separator": "1.2.0",
+ "@radix-ui/react-toggle-group": "1.2.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.4.1.tgz",
+ "integrity": "sha512-NxLFGSFtZ78zVfcoOhTwR1xwqo5qx1Jh8Pw6FMZ9McCaqfRzz7/gIRahs2Oy5XlPgQ+8QkUcKbN5B0U44J/kCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-id": "1.1.4",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-visually-hidden": "1.2.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.5.tgz",
+ "integrity": "sha512-Y5GBf4NKeADoXI1geiMlMuMoURT3k9MizzzKhGb/m55H94aGigZ01PIpkvsO6lML13BA6XYAXSJ6gza7vTn5nw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.3.0.tgz",
+ "integrity": "sha512-TAcBI7bw2ljYCbv4+zuTSd6zICu8pRt6SDN2vJ/Z06K2MXh6I9WYFe/A9ovlFDtR5HVHGm5iDxDoABm/++dbKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-use-effect-event": "0.0.5",
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz",
+ "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.6.tgz",
+ "integrity": "sha512-9cRullnQRv+kqnRmFlmh0WC3kAPuGmHiQ9VwsXdrYk//sBhgCKEttcpPEPgIQbyx6TGUmooVEHYTyohwz/lSJg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.5"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz",
+ "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz",
+ "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz",
+ "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz",
+ "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.5.tgz",
+ "integrity": "sha512-V1McDQkMGzgivwqdu7u96APs7oi0Jvj9LTzgq3WWrDZqxWdAqU9yZnZQqk5OizGFR8badUQwZsrlao7MOlWYOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.13.tgz",
+ "integrity": "sha512-feijiN1oPk4rck4u9Dm9tsU4mT6Wp9oeEEhaz1WUEs1VuqLf8xzsFG+Qk+uWq6TnMH9O+WRECRg0NftyP9E4Fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz",
+ "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==",
+ "license": "MIT"
+ },
+ "node_modules/@react-three/drei": {
+ "version": "10.7.9",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.9.tgz",
+ "integrity": "sha512-wP4Y6o58KFi+hT1zCfjJarRtPCT6WW/lo53WMiA/aCORuUvBzW916XsCtadnIB1KB+3IQNcDU5JeVh/gYb8MYw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^3.1.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.8.3",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.4",
+ "tunnel-rat": "^0.1.2",
+ "use-sync-external-store": "^1.4.0",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^9.0.0",
+ "react": "^19",
+ "react-dom": "^19",
+ "three": ">=0.159"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "9.8.2",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.8.2.tgz",
+ "integrity": "sha512-6s0ejw0bT1myQ/aLLugcil8/zBmNnnHw/AISWF1yfDLOxfX3yYaTS+0IkpJITEnm/SSMLtxje6CJDt0i5Wu1Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^2.1.1",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.28.0",
+ "suspend-react": "^0.1.3",
+ "use-sync-external-store": "^1.4.0",
+ "zustand": "^5.0.3"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=19 <19.4",
+ "react-dom": ">=19 <19.4",
+ "react-native": ">=0.78",
+ "three": ">=0.156"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@remix-run/route-pattern": {
+ "version": "0.22.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz",
+ "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==",
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.19.tgz",
+ "integrity": "sha512-e+Ebe0WT+9tbXmNKPIiC8YrWAHYwug3m6i4p3rZGGulfZri1jtI4HslT+OZnFf/OIHowWF71DTlHwXBP2SiA7A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.19.tgz",
+ "integrity": "sha512-hOombrTJ+szt2ciEzS7oCA78lMPBgF5UMQWCJIyNfnrI7GLl8HYoFqCnpL2gcPOaXMqryGCU5ExdQKyDDcC+yA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.19.tgz",
+ "integrity": "sha512-XoMA2pwCKGTQzUjPgtRwkedH4WmXhdIPKT6l6oOxd1PUbNodGI6DcBekgN7EC//+x3pfBYX/qL1gNmZAqivw5g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.19.tgz",
+ "integrity": "sha512-zo8EsSSwMZCY/e+fRnGaX02YGgvJD0yvhNi/9hpGPA2MJ9ZF8dPCw75UGfpI6OpQ/myR/dIYmogRvqiZD0TrXg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.19.tgz",
+ "integrity": "sha512-O8ISAOfI1nj7AuBPd30FSF47z7f7lzlk5poQVGgiikvQuGEBFYvW8ki+UEnT+YJPnNGjQzn304klt4O9FUE7mA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.19.tgz",
+ "integrity": "sha512-/lYCba5NO0yq4S0jGMH8QTBtkvQxoXjoQWzF5BcWp9MjB9jxVRD1GQDNofkVjcOj30DWyhLIN/4Oe60JKzaONw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.19.tgz",
+ "integrity": "sha512-pm/apFMBnSbFb3O6nG5pHO1iqHhZrjGDFEjSm+ot9J17XXPvH9L90y5h+jgfHCE+Qdlsvy8qKEcU4VKxgjD+vA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.19.tgz",
+ "integrity": "sha512-K1SmQp3FFichn8oqrNIwvFoZ5wcvjC7goBzpaMNvLf9FZc+QB/QvLmnpJZgAAGHiD980tP8FZZnXk2K94rvlxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.19.tgz",
+ "integrity": "sha512-I0xwD57jP/3A7+5UPZin2w8Y7XHEmEM5zqpYZOcQ45MlNW6tUJUziymV3yRr7rL4vwyfEcRsRksC4a7Kc08p5Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.19.tgz",
+ "integrity": "sha512-BrtEHT24SNXj9HiXqy+s/kXiSiQGt6EKCPUlRirAoIioVxKGnbXBUEkbZASAei7iZJB0/Ynm+0JP00Ipx7AnSw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.19.tgz",
+ "integrity": "sha512-rm75/9DkHKeU2WnA8w4X94WGYgLlDCFCfkQC+4bguVJmC/jvDRv2B+2wLOYhyE0fVuv7//Ckk39OoOufyCu76Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.19.tgz",
+ "integrity": "sha512-irh/4+iYFM4GAzpluDErgjz89bj6NDtqqMAG/6O1LG38Ed3NpyebzCpcVNkFW8uhy+/3/BocUvnXDR33CXWe9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.19.tgz",
+ "integrity": "sha512-+KWTWNoPt+CfSq94CiceWu3yPE+QzkaG+phPyz9G60tmZLwfQkcA4AeksafzvJAw4pvg1mcd5a1TkUqVv0s4JA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.19.tgz",
+ "integrity": "sha512-7EH+skOsl5qpWnK0vzamgJFhOv1cwxQpsxm8tI883K9fur5c27p8q87fzEO6BbBPabA2/+s/zR63/pLc137iOg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "2.0.0-beta.2",
+ "@emnapi/runtime": "2.0.0-beta.2",
+ "@napi-rs/wasm-runtime": "1.2.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.19.tgz",
+ "integrity": "sha512-3Q1Yg1Q7tEvBWoZf5rFvG/GKZQPgwfru+p4wBajDfJFBs0SHCpL++dfTjW9gyAKd1T7ZpIYvdaUax7Pa/IhFVQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.19.tgz",
+ "integrity": "sha512-y/sqRRc6d7hECzOFc5n3LSeqctmIp9mMjY1Kq4s14uLPd/LZ4z7i1zrW6fMtp2mPeuQIC4e6ULMcwQzMomqQag==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.1.0.tgz",
+ "integrity": "sha512-K+NU2HDCEem4sClgHH4ZZXwdJeNFkJo+hRk479DIwdIviAPN2hFJH1KHHwg8CxHQXvi0M5XY4deTXINfIiSG6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.5.tgz",
+ "integrity": "sha512-yT53FzV4ke0cr/BguDxhWMpm5MJu1O2muzk+tKw6lKHjODAsYArBbWwFbJiaksfzFv1tpkO9QHGzmfs1hJT66g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.24.5",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.33.0",
+ "magic-string": "^1.1.0",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.3.5"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.5.tgz",
+ "integrity": "sha512-PynqGLZsZvwM9tqD/CUxkOIfyd6K5elZ2MooauzJryP/UqXYgi3yYbtt3N0Xe+MHV/eFKPFQCEfIKAqlAS1CBg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.3.5",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.5",
+ "@tailwindcss/oxide-darwin-x64": "4.3.5",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.5",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.5",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.5",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.5",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.5",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.5",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.5",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.5",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.5"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.5.tgz",
+ "integrity": "sha512-Se0z5xMqcSQanJD1IYmO0cGpK3iv0fp3ZsIT7xRJZ1Da5PghvNoc46zz4jIQn+aan3dV9T8AermFNN9QPmsh0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.5.tgz",
+ "integrity": "sha512-UPrgTDuUbvmqGMZuCnmI3FKee/tYKzKdp00/kuB4oSGB213VYg8QTdF7lVhnPoBXq/mIfJ+ILoBFqNOboWcMwQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.5.tgz",
+ "integrity": "sha512-E+p7HEtjI0b8D7kbbWxH067cGui6d4c79COGko9fAPVE3rwc1E7ld2HFCbaG86AOGWS5VKQYqZOaf38ya4uxTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.5.tgz",
+ "integrity": "sha512-yWEWcWyAG7rWOcTpJTjdhQ3pcaFY2h+0bqTRfgU3G2fz87ZU3AKdvdZKhslD+kyHnjBUWKvNdRYwIYC5dI1tIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.5.tgz",
+ "integrity": "sha512-WmeVjtJhZlr+smpqcdR2kQkkn6arHIJnUIBxTO2EJcTLGY1ecVXNo2hbSn8xc0IH2aEtys936iMWKGyj0GaKnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.5.tgz",
+ "integrity": "sha512-DTCSyucc6ShMp1QMqV5R94Z7xlgNgUzkGmFF1rL5HH2Qjy4eSmEqy+YLGkqqYvWa4sILtgrBt1JjRKbneQtiFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.5.tgz",
+ "integrity": "sha512-H9lsvO4x9E4nE4Hi8h7NHHw0O/kcVSu+iOyFHNGdYzazGgDjFHZbE6S3hQ8kMcbNXSBlMdNi67LV5EVqtpaJtw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.5.tgz",
+ "integrity": "sha512-4uxQvtBvPuIdl13WfUBj9xMZHH14PtHZ8OZdtLmdyDtG66sCa1t599NZT+jagkUzRtWhuul1qRZQ1nAJscXN5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.5.tgz",
+ "integrity": "sha512-0JQAzs7jwbA7O6+z/adU+Jk3AXwsu3npAhtr5escXS7TqEqWcaDV7qHuI+0Vy/KQRuVGnLQREcSJWYauyV3oqg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.5.tgz",
+ "integrity": "sha512-OJhFmlEfdzMr+avACdi40RPtnkW0B6hXyxL/8r+tnEHBWZji36CYRjEXrrvgBmaYsD3oEOBqwYFRKsYvzFe7Bw==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.11.3",
+ "@emnapi/runtime": "^1.11.3",
+ "@emnapi/wasi-threads": "^1.2.3",
+ "@napi-rs/wasm-runtime": "^1.2.2",
+ "@tybys/wasm-util": "^0.10.3",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
+ "version": "1.11.3",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.3",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.3",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.2.2",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3",
+ "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "dev": true,
+ "inBundle": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
+ "version": "2.8.1",
+ "dev": true,
+ "inBundle": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.5.tgz",
+ "integrity": "sha512-9o8jgthi+QE5eandnboiJT64VQfnIArfZzSTdgbhEK5aafdiu2jTW9Honi/mwYHxBZuH1vAC57IFyrh+0iK5Gg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.5.tgz",
+ "integrity": "sha512-XiKFfIy1RWe7qK4WqByxNW7Oz1/nkOy8o/BWhHminyV1PmpUd8Eedj5v2A8h2kpqeVlz6xRnwk9Fr/ZXNLcZfg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.5.tgz",
+ "integrity": "sha512-BCNiY0a1ZjdOHVqw7kyb7U72BwfMF3dfUuN+tQsLWky/J5JOI/r7abibglXfygrZhVO/3u3V+E25kkKTeMkXaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.3.5",
+ "@tailwindcss/oxide": "4.3.5",
+ "tailwindcss": "4.3.5"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.107.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.107.1.tgz",
+ "integrity": "sha512-PSyAik11oQBEVOiVy5hjZWHmoUqOXqK1WYy4v55mcxozEfKaQgpUMsgnXhWjuESzSht8JfzqMS1m6M/9CpNgvg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.107.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.107.1.tgz",
+ "integrity": "sha512-udCOOLp3ZAheUrIHLCe+ZZge6nr2ZUrGMSWcCuXsjY1Bf2JeeE09oxst97PILiCY6N8xo/7bIo3Hvz13zh06FA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.107.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@types/react": "^18 || ^19",
+ "react": "^18 || ^19"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.2",
+ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.2.tgz",
+ "integrity": "sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.10.4",
+ "@babel/runtime": "^7.12.5",
+ "@types/aria-query": "^5.0.1",
+ "aria-query": "5.3.0",
+ "dom-accessibility-api": "^0.5.9",
+ "lz-string": "^1.5.0",
+ "picocolors": "1.1.1",
+ "pretty-format": "^27.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@adobe/css-tools": "^4.4.0",
+ "aria-query": "^5.0.0",
+ "css.escape": "^1.5.1",
+ "dom-accessibility-api": "^0.6.3",
+ "picocolors": "^1.1.1",
+ "redent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.3",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz",
+ "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@types/react": "^18.0.0 || ^19.0.0",
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz",
+ "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/aws-lambda": {
+ "version": "8.10.164",
+ "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.164.tgz",
+ "integrity": "sha512-XOnrazWcOd6yWPnR7DxqCBPPYciDjHq+NN8LPwwqBCJNXLyPJgKo47fZpJ113+oyAn9Zti0Cf6H4k6yPcE3WFg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz",
+ "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz",
+ "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.10.tgz",
+ "integrity": "sha512-lTyOMnOcmRBzdCv6/4zUZCrEhV0xrqQiUZBZtVcbmLHBXKLnN8q89v/wKRF/Q0HbwVsqbIG3o5HDoFl9sduM+g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree-jsx": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/hast": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
+ "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mdast": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.19.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz",
+ "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": ">=7.24.0 <7.24.7"
+ }
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
+ "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.3.0",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
+ "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.3.0"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.187.0",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.187.0.tgz",
+ "integrity": "sha512-v+vKNarUtk4n2QRZ4+CN5ygMM0ARUjXjIQl0R/unMUfmStFeVqGbgqarbrpoGTXRZoulSH16ePrVJVxPJEbMUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.3",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/unist": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.2.tgz",
+ "integrity": "sha512-222PKFXKihmFpi/pS5njoBgdAqsPhj2/GBmFbrNw8Tq8gEG9Sv+kSlKFy4u2q+mJv9ZzJgM2Gkv/bUzRZwZw5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.72.2",
+ "@typescript-eslint/type-utils": "8.72.2",
+ "@typescript-eslint/utils": "8.72.2",
+ "@typescript-eslint/visitor-keys": "8.72.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.72.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.2.tgz",
+ "integrity": "sha512-llP+T1YuJOLlxEciP6OyQGtz5SMxyn+9LA2BvZw5RmToGVa1y4beEDVQL6Gje7T5WFYh19WREjDCN9Ev4gm5AA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.72.2",
+ "@typescript-eslint/types": "8.72.2",
+ "@typescript-eslint/typescript-estree": "8.72.2",
+ "@typescript-eslint/visitor-keys": "8.72.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.2.tgz",
+ "integrity": "sha512-6y5dLTZdHhI8Eh3yIQ/bfQhRKwn5e+TpJj64o18Cngc0iW/BCrVvrr6zGYqXPnYH31u6YXAYiFh0+qxX5mybJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.72.2",
+ "@typescript-eslint/types": "^8.72.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.2.tgz",
+ "integrity": "sha512-iIoMJ6/8c1/QpFNyFjU0d1YzPco4MsZjQw0k3V+vfzNWAFmUmvhlyMoYEoYYDQZXc+J6Gi1X5XcxlzW2taIIGQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.2",
+ "@typescript-eslint/visitor-keys": "8.72.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.2.tgz",
+ "integrity": "sha512-+h3iCgZXuTNtoLXcnnjP12ZUHLMqoUcIHPfQrFEXTUqvb9dF8QK9b5nv5VtSo5fLkdHuks8ehbTzZj3rImAm9w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.2.tgz",
+ "integrity": "sha512-STfkR38qIdhfkLpgtXnUjAzsNy4xouqiBO0lrVc/Wpk9Kem5/aHXUx1J8dGC16zpdYN1+yc2ILQ28er1kXX7ag==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.2",
+ "@typescript-eslint/typescript-estree": "8.72.2",
+ "@typescript-eslint/utils": "8.72.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.2.tgz",
+ "integrity": "sha512-Xfn37OLZx03+PCU7qWx9DTv2FCRh1E6fk2+wONeNY15HXIwtcwuveaSwVpGAUptkNmPQegRxW5JyRel9T7wrog==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.2.tgz",
+ "integrity": "sha512-pQEGqIK/cqEvKivVWaFLYpe7XYejFxfuAOi4mu1oAWFvIo36aRHI9esRFTUxcIy/z8N482RJyH+LpZB0ZIU7Ow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.72.2",
+ "@typescript-eslint/tsconfig-utils": "8.72.2",
+ "@typescript-eslint/types": "8.72.2",
+ "@typescript-eslint/visitor-keys": "8.72.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
+ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.2.tgz",
+ "integrity": "sha512-hv3eRYbpPbbpezuax1EfGrzpa3C4ySgNZkPUkLOWOAjU8HsOKowEDAr8C7nsNl1C+SBSKDpvno6VKHnlhvp9iA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.72.2",
+ "@typescript-eslint/types": "8.72.2",
+ "@typescript-eslint/typescript-estree": "8.72.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.2.tgz",
+ "integrity": "sha512-NAGrjn0Pitjqzq/GkgMC4kBYm233PQ8GffyQ9+kTA3AlNQYEzOf25EIM02BAHZv5FRJSpHpJk+OGUxVkOB93GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.72.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz",
+ "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
+ "license": "ISC"
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vectora/graph-engine": {
+ "resolved": "packages/graph-engine",
+ "link": true
+ },
+ "node_modules/@vectora/graph-layout": {
+ "resolved": "packages/graph-layout",
+ "link": true
+ },
+ "node_modules/@vectora/graph-schema": {
+ "resolved": "packages/graph-schema",
+ "link": true
+ },
+ "node_modules/@vectora/language-adapters": {
+ "resolved": "packages/language-adapters",
+ "link": true
+ },
+ "node_modules/@vectora/local-engine": {
+ "resolved": "apps/local-engine",
+ "link": true
+ },
+ "node_modules/@vectora/repository-indexer": {
+ "resolved": "packages/repository-indexer",
+ "link": true
+ },
+ "node_modules/@vectora/search": {
+ "resolved": "packages/search",
+ "link": true
+ },
+ "node_modules/@vectora/shared": {
+ "resolved": "packages/shared",
+ "link": true
+ },
+ "node_modules/@vectora/web": {
+ "resolved": "apps/web",
+ "link": true
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.1.0.tgz",
+ "integrity": "sha512-OY9aShBQG5KOIw2zCo9fDoCHI9RHliNHJ//nM7m6jGg41Hyke9JOGWmeMlK74xYErsxMoK8mIB3gAeEyFVA7LA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/istanbul-lib-coverage": "^2.1.0",
+ "@vitest/istanbul-lib-report": "^2.1.0",
+ "ast-v8-to-istanbul": "^1.0.7",
+ "magicast": "^0.5.5",
+ "obug": "^3.0.0",
+ "std-env": "^4.3.0",
+ "tinyrainbow": "^3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "5.1.0",
+ "vitest": "5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/istanbul-lib-coverage": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-coverage/-/istanbul-lib-coverage-2.1.0.tgz",
+ "integrity": "sha512-WOjuS+xcjSIy7ruYBQ+9BUcV7XZQ0FyVcbWbWxW9TRx/kLGx5SQaeFRIFDJxjNplb6YsT7SpKo2zuXXUo91OIQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@vitest/istanbul-lib-report": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/istanbul-lib-report/-/istanbul-lib-report-2.1.0.tgz",
+ "integrity": "sha512-+ZFta25nWtGCOBG9/IET8w1RO5Oawsd6H8Uf6AJ9EA/StFuKVtKAEJEJ6uRt84OH+T4YkI0ASmIHAhRA+0KVxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/istanbul-lib-coverage": "2.1.0"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.1.0.tgz",
+ "integrity": "sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.1.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.4.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9 || ^3.0.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.1.0.tgz",
+ "integrity": "sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@xyflow/react": {
+ "version": "12.12.1",
+ "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.12.1.tgz",
+ "integrity": "sha512-g41OHmP+GL5wDBQKpgYt8AF6RlB4CrABtiStXhCMqdItK6gPdSpsuDk8V0YtdyvIvMPTjDJvkuLkz0JbSYu5Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@xyflow/system": "0.0.84",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=17",
+ "@types/react-dom": ">=17",
+ "react": ">=17",
+ "react-dom": ">=17"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/react/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xyflow/system": {
+ "version": "0.0.84",
+ "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.84.tgz",
+ "integrity": "sha512-96zfGFjH3oZ3T0MRdH4gJ0SlXuCIxbBuHVevZ4xry5UTxqjzjt010+LRdholgepq7jhrcaz1JNtzrETscwnkEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-drag": "^3.0.7",
+ "@types/d3-interpolate": "^3.0.4",
+ "@types/d3-selection": "^3.0.10",
+ "@types/d3-transition": "^3.0.8",
+ "@types/d3-zoom": "^3.0.8",
+ "d3-drag": "^3.0.0",
+ "d3-interpolate": "^3.0.1",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0"
+ }
+ },
+ "node_modules/abstract-logging": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
+ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
+ "license": "MIT"
+ },
+ "node_modules/acorn": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+ "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ajv-formats/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats/node_modules/fast-uri": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
+ "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.8.tgz",
+ "integrity": "sha512-lPdpagk13ANXV+HClJEpCZM7kUsl5XcvgqKzZhaj6Ve1ZFICcY2sGhAN1n1vT0tML6EclT7bfVgTxqL57Gkpow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/atomic-sleep": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+ "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/avvio": {
+ "version": "9.3.1",
+ "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.1.tgz",
+ "integrity": "sha512-QyTKW61/qlba+cUQBm/sT9X8lri4UdomqSDwCnrnj1IvLVDRRiXRkz4CgBRWB+/cmfC7SK5WQc8T1vSdGvaUcQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/error": "^4.0.0",
+ "fastq": "^1.17.1"
+ }
+ },
+ "node_modules/bail": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.36",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.36.tgz",
+ "integrity": "sha512-AbPFGS4K9S3meMRCLTM0Zas+e9X7b5ygUBOboDT5zevCZt9ByKSbMtKhwyVqW+oL8WemPVsbP7X3rdZcIyOa0w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bcp-47": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.2.tgz",
+ "integrity": "sha512-wsv86UuNFuPnVieKLyECTT+CP1WSAHeoIg5P3xu2kA9Z+7fVPoKt7dxzSeEhtJel0AI+NW3fIW3KtTKQKUCngg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/bcp-47-match": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz",
+ "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/before-after-hook": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
+ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
+ "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/bottleneck": {
+ "version": "2.19.5",
+ "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
+ "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==",
+ "license": "MIT"
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.13.tgz",
+ "integrity": "sha512-Vh3dmFbc4wZeox7M9I/0L9GfFd83Su4+yCck/OF9Gi/P/dAHn0RT7X4K9R7gMNMhDck3DvhR6QXsnQdjjgwNvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.29.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz",
+ "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.33",
+ "caniuse-lite": "^1.0.30001819",
+ "electron-to-chromium": "^1.5.464",
+ "node-releases": "^2.0.60",
+ "update-browserslist-db": "^1.3.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/cacheable": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
+ "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cacheable/memory": "^2.2.0",
+ "@cacheable/utils": "^2.5.0",
+ "hookified": "^1.15.0",
+ "keyv": "^5.6.0",
+ "qified": "^0.10.1"
+ }
+ },
+ "node_modules/camera-controls": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
+ "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.0.0",
+ "npm": ">=10.5.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001826",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001826.tgz",
+ "integrity": "sha512-wTB+9tIbLro/BCXwLuNV1meHE4L9dS6dArMDwgYfNBPMYScqClHyGunhkCSDtHhe/69Xl0sHlKgBVZ+fNsYtWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/ccount": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-html4": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-entities-legacy": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/character-reference-invalid": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/cmdk": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
+ "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "^1.1.1",
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-id": "^1.1.0",
+ "@radix-ui/react-primitive": "^2.0.2"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^18 || ^19 || ^19.0.0-rc"
+ }
+ },
+ "node_modules/codemirror": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
+ "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
+ "node_modules/comma-separated-tokens": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
+ "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz",
+ "integrity": "sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-3.1.1.tgz",
+ "integrity": "sha512-GW4qUsfFo59d0HbUibDlWv5wPz+vAAcaTWbKIuKCf0JkC7wWkSyf8f13IpXn5JkeMlB2P8iTSSIjwXljorg2vA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.2.tgz",
+ "integrity": "sha512-SL0cVuEdETEp7pN0FuZb01toc3vnODEfVUCooxG1SJN0yIirh7JMb9X9bvfCFf0vzzbT68RdbKDHYTiWy7a7iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cookie-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
+ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
+ "license": "MIT"
+ },
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT"
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-binarytree": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
+ "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.1.tgz",
+ "integrity": "sha512-lfab8x0Bj0UO7SNsY8dZSbNIA6ipbkE3X0NG9nqjCu6CfCHZseuDnOSJY7hidKrjpP++i1M2qT+FlAdPKYRilA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.2.tgz",
+ "integrity": "sha512-sAzH4Y4nsLo8eSqw5jKiZt+LU2wZmL/qS+OkxQGjhwazB/D7TY34R/QK1eshfGr7cZEKymKZ/Haem7m+lu98Sg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.1.tgz",
+ "integrity": "sha512-OWeoayCq8J69hUIjvL+Mb6Q6Oaj/fyHV5XaXByEUAG+JUjIfTGDnk3qGpcXrD3VmQ3BSV96fcbM6fqq7C0tbPA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-B5znKqRmKROAN4oHWQ50W/qisVjA9GXh2WMsVKdFVp4DxWmcrOsweqgUOUVsv5th96kwaZlokU6SlV4J3gak1w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force-3d": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
+ "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
+ "license": "MIT",
+ "dependencies": {
+ "d3-binarytree": "1",
+ "d3-dispatch": "1 - 3",
+ "d3-octree": "1",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.2.tgz",
+ "integrity": "sha512-IRzGU62DtxhvsnSTWMQIdnoZ6gA+BCWwBon1EJI6SjoN595799b6z/QRBzXfFhE8iupUPp3042yBNo0STPqnzQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-octree": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
+ "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
+ "license": "MIT"
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.2.tgz",
+ "integrity": "sha512-9Lu9mqJKRbIk0d6mKDjpvPURaPdwHVK5CYbcdd0/4vBgz4ldz/7TIOse3kNERsByGNfld58KEco507AztJWyTQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.1.tgz",
+ "integrity": "sha512-ODSf35vqCrlCfp7hrm0eZjvOosSXSYLjqvIlhMBV9q9VOx7EBJG6pc+uXBswEZNJgCGJ+qnKYcWutbZDIxe/Xg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-1Il1aliroVSErrv5aulRBxAY+7+DnaoLIWGd1C6SsBTWDeobWiP/V69vwb8vT6abR1kKwaNe1196pmaQ1fK90A==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.2.tgz",
+ "integrity": "sha512-RcjCsdFgyFoDB+QXLC5m60ZkTN0k9zH+ovoLlTdJnDS1AbfbcuowiM4pCKSTOclNibS4f4J4cbv2m7O7y8NTOQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.1.tgz",
+ "integrity": "sha512-kfjD3BDQu/FOatpZ6JdMSyIoDwTxIpI29ySTI2J5ZlDaH1WbuJGQJ2Q9oJkWTEZEW98j6idezoomSD3Cn2zwiw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-8.0.0.tgz",
+ "integrity": "sha512-JQUCCK+/CAGbGGx618PlFZMXH81Jtj7t73CHv9dd/zVWvxgGT2zLFRjnKb+Ng46GIwcy+f7f88qd4duqllK+WQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^17.1.2"
+ },
+ "engines": {
+ "node": "^22.14.0 || >=24.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+ "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/devlop": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.9",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.9.tgz",
+ "integrity": "sha512-MqC0eAfW5gPATGY3GABIyFWpHwdDGAyewTdek5R/e+mhI9ZDUMKb2mTDkTMUW0+Vm7an+ThEMUop3+svS72Uug==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.481",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.481.tgz",
+ "integrity": "sha512-dNUsbAP6E2S1dHIYEuN3cQ0dViThjMtpVRheGdvv5HgHFtzLi4Cu4Mbzms85YggP3Eh2VVZTl5+XxEN8ofCbmQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.26.1",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.1.tgz",
+ "integrity": "sha512-PPnnSJq3azKOIewXR/uppBV/6h98dprBJ/WaQ9Qgj10xX1lJLNfg+XShpbsrSqcDSy2yYYyhfXM1wWhftqgyzw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-9.0.0.tgz",
+ "integrity": "sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.14.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.14.0.tgz",
+ "integrity": "sha512-T0pNsxkb9IKmtQjhpK3d7nWOpFv+YHuMbTJnSVMoQblF0onvXfErrtp4V1jHtsQPan7dKaGVD0q1WCXnzMF31A==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.7.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.3",
+ "@humanfs/node": "^0.16.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "11.1.5 || >11.1.6 <12",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.6",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-util-is-identifier-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/fast-decode-uri-component": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+ "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stringify": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.3.5.tgz",
+ "integrity": "sha512-g7H3xqJmdH9aWLWh8S3CqrRZFZ321yaeNHZp3QUDl3wtXDWjPChDXCDDTwA4M/zsU42nOhE+z5KmpCzgBaaCgw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/merge-json-schemas": "^0.2.0",
+ "ajv": "^8.12.0",
+ "ajv-formats": "^3.0.1",
+ "fast-uri": "^4.0.0",
+ "json-schema-ref-resolver": "^3.0.0",
+ "rfdc": "^1.2.0"
+ }
+ },
+ "node_modules/fast-json-stringify/node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz",
+ "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-querystring": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+ "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-decode-uri-component": "^1.0.1"
+ }
+ },
+ "node_modules/fast-uri": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.3.tgz",
+ "integrity": "sha512-I1PXwP5eAhlNlcdpjt9CgB59op2ZvZfxw+BkqGFVIUOgNpCVTcTqp3HhTX7brxj5vsip+nSY4nTvff+eIA76hg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fastify": {
+ "version": "5.12.8",
+ "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.8.tgz",
+ "integrity": "sha512-moP8exb2sCHLUbJkwp+I6C0padhy4D8lp0SdmzY7HhSdZuqm7O/u3o2aq7u8okaQ//33QDzj3G/u+FsSdB8Qzw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@fastify/ajv-compiler": "^4.0.5",
+ "@fastify/error": "^4.0.0",
+ "@fastify/fast-json-stringify-compiler": "^5.0.0",
+ "@fastify/proxy-addr": "^5.0.0",
+ "abstract-logging": "^2.0.1",
+ "avvio": "^9.0.0",
+ "fast-json-stringify": "^7.0.0",
+ "find-my-way": "^9.10.0",
+ "light-my-request": "^6.0.0",
+ "pino": "^9.14.0 || ^10.1.0",
+ "process-warning": "^5.1.0",
+ "rfdc": "^1.3.1",
+ "secure-json-parse": "^4.0.0",
+ "semver": "^7.6.0",
+ "toad-cache": "^3.7.0"
+ }
+ },
+ "node_modules/fastify-plugin": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.1.tgz",
+ "integrity": "sha512-4wW4RxSmvPXLrh5mgQEiu43qU3bw44N2ss4FTkTkQIbprUhE+/s43QX0j/ttcztdWK7PEkHqVthdc5GdEGDvjA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/fastify/node_modules/semver": {
+ "version": "7.8.6",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz",
+ "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz",
+ "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "11.1.5",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
+ "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^6.1.23"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-my-way": {
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.10.0.tgz",
+ "integrity": "sha512-eru+uCPQ2g+nLBkf84MHP/KyNNMar0BDy0j+i8Fb72GF9/YHBZ58q7FODUtfVV7XU9nfRUHXSmaM8NmZcIbZ4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-querystring": "^1.0.0",
+ "safe-regex2": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "6.1.23",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+ "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cacheable": "^2.5.0",
+ "flatted": "^3.4.2",
+ "hookified": "^1.15.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/glob": {
+ "version": "14.0.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.5.tgz",
+ "integrity": "sha512-nbKW5Sl53DEM5PkmZ5Jwfeyss1ohuqcgOBjOarSjz1fp8W4ss4EOOvKADQRbApD6JmVBX+HkpZYfBez4wXbeQQ==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^11.0.5",
+ "minipass": "^7.1.3",
+ "path-scurry": "^3.0.0"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "11.0.8",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.8.tgz",
+ "integrity": "sha512-nFQuYSTVrGvysVBps5Ih+0BdnYFN/Cj3C816r6+p7yK+FHrVLGE4O4N9AFwykrlNlvM2HYt+Rad1Uiro6h88Ig==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/nonrecursive-glob-match": "^1.1.0",
+ "brace-expansion": "^5.0.12"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/graphology": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/graphology/-/graphology-0.26.0.tgz",
+ "integrity": "sha512-8SSImzgUUYC89Z042s+0r/vMibY7GX/Emz4LDO5e7jYXhuoWfHISPFJYjpRLUSJGq6UQ6xlenvX1p/hJdfXuXg==",
+ "license": "MIT",
+ "dependencies": {
+ "events": "^3.3.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.24.0"
+ }
+ },
+ "node_modules/graphology-communities-louvain": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/graphology-communities-louvain/-/graphology-communities-louvain-2.0.2.tgz",
+ "integrity": "sha512-zt+2hHVPYxjEquyecxWXoUoIuN/UvYzsvI7boDdMNz0rRvpESQ7+e+Ejv6wK7AThycbZXuQ6DkG8NPMCq6XwoA==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-indices": "^0.17.0",
+ "graphology-utils": "^2.4.4",
+ "mnemonist": "^0.39.0",
+ "pandemonium": "^2.4.1"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-indices": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/graphology-indices/-/graphology-indices-0.17.1.tgz",
+ "integrity": "sha512-ZY5zVyAVPJh/x+Topc+lcW+z2HHPn4YjhPS/orydotmmIhQ4dA13lK4KAD2RfPdsU8Z3HGwWQaBthQkJaTAF6w==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.4.2",
+ "mnemonist": "^0.39.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.20.0"
+ }
+ },
+ "node_modules/graphology-layout-forceatlas2": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/graphology-layout-forceatlas2/-/graphology-layout-forceatlas2-0.10.1.tgz",
+ "integrity": "sha512-ogzBeF1FvWzjkikrIFwxhlZXvD2+wlY54lqhsrWprcdPjopM2J9HoMweUmIgwaTvY4bUYVimpSsOdvDv1gPRFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.1.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-layout-noverlap": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/graphology-layout-noverlap/-/graphology-layout-noverlap-0.4.2.tgz",
+ "integrity": "sha512-13WwZSx96zim6l1dfZONcqLh3oqyRcjIBsqz2c2iJ3ohgs3605IDWjldH41Gnhh462xGB1j6VGmuGhZ2FKISXA==",
+ "license": "MIT",
+ "dependencies": {
+ "graphology-utils": "^2.3.0"
+ },
+ "peerDependencies": {
+ "graphology-types": ">=0.19.0"
+ }
+ },
+ "node_modules/graphology-types": {
+ "version": "0.24.8",
+ "resolved": "https://registry.npmjs.org/graphology-types/-/graphology-types-0.24.8.tgz",
+ "integrity": "sha512-hDRKYXa8TsoZHjgEaysSRyPdT6uB78Ci8WnjgbStlQysz7xR52PInxNsmnB7IBOM1BhikxkNyCVEFgmPKnpx3Q==",
+ "license": "MIT"
+ },
+ "node_modules/graphology-utils": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/graphology-utils/-/graphology-utils-2.5.2.tgz",
+ "integrity": "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "graphology-types": ">=0.23.0"
+ }
+ },
+ "node_modules/hashery": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+ "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/hast-util-to-jsx-runtime": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.7.tgz",
+ "integrity": "sha512-F7DNV2NMcTxA4GqPVHnXzX+mpoPHAIY9rJAVH3CZ1FdBZOcBUEjUuvMkMpHVs/sicEtznh9tMGj5WUR9S4E2rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/unist": "^3.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "devlop": "^1.0.0",
+ "estree-util-is-identifier-name": "^3.0.0",
+ "hast-util-whitespace": "^3.0.0",
+ "mdast-util-mdx-expression": "^2.0.0",
+ "mdast-util-mdx-jsx": "^3.0.0",
+ "mdast-util-mdxjs-esm": "^2.0.0",
+ "property-information": "^7.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "style-to-object": "^2.0.0",
+ "unist-util-position": "^5.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-whitespace": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
+ "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/hls.js": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.4.tgz",
+ "integrity": "sha512-3GlxGdPvT2YHvQYOt+yOzT/DEior/qJVNIylHA77kgeoBTdQQqi70GwefNEUmHQeNGyjC8U62+bruqW/Tpm+tg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/hookified": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+ "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz",
+ "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.15.1"
+ },
+ "engines": {
+ "node": "^22.13.0 || >=24.0.0"
+ }
+ },
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
+ "node_modules/html-url-attributes": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
+ "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.9.tgz",
+ "integrity": "sha512-IttN8BqKLxzUrnTqHI+/hwAJQItBAUjdoHERCDv+oOuX48VP6bzW8+QjOXSfoevzq3YrYAwX1uLHp8TqO9yIeg==",
+ "license": "MIT"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz",
+ "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/is-alphabetical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-alphanumerical": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-alphabetical": "^2.0.0",
+ "is-decimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-decimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-hexadecimal": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.1.1.tgz",
+ "integrity": "sha512-C5ZoVkeqSt0Aje0tzESO+iT/EQpNFNL39GkP7mCACtoZUFD6GcyerFCr6kp0ZtiII4W9HoYkY48Soy5xE/uHKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.9"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsdom": {
+ "version": "31.0.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-31.0.0.tgz",
+ "integrity": "sha512-xAkhD9z1eyFC5+QUi9K24CaZJgdCQ2GOGLW1GdVDoQvkp/AxEH9TQ0pAplPOlIiV92hfcgnFOADZypbjgbBT6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.16.0",
+ "bcp-47": "^2.1.2",
+ "bcp-47-match": "^2.0.3",
+ "data-urls": "^8.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^7.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.5.3",
+ "parse5": "^8.1.0",
+ "parse5-parser-stream": "^8.1.0",
+ "saxes": "^6.0.0",
+ "tough-cookie": "^6.0.2",
+ "undici": "^8.12.3",
+ "w3c-xmlserializer": "^6.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^17.3.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ },
+ "peerDependencies": {
+ "@napi-rs/canvas": "^1.0.13"
+ },
+ "peerDependenciesMeta": {
+ "@napi-rs/canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/lru-cache": {
+ "version": "11.5.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-schema-ref-resolver": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.1.tgz",
+ "integrity": "sha512-w+HwgljXpVwZ3IidoUR1qPZxSOQ+NRQWuVe6a5pe8XbHsNZE4UAwVPUvzZagn7yBKbJtt6UJnRFYbNTRPZI45Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-with-bigint": {
+ "version": "3.5.12",
+ "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz",
+ "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==",
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+ "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@keyv/serialize": "^1.1.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/light-my-request": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.8.0.tgz",
+ "integrity": "sha512-5Q8PjoSEdtGIB8fikZsTem5vgRo4F2mR6vUKTDW/H1g6MtAt8e3duQbg/QxL5yzg6BZhor2KOUm3hhWMWLTZwg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@fastify/deepmerge": "^3.1.0",
+ "cookie": "^2.0.0",
+ "process-warning": "^5.0.0",
+ "set-cookie-parser": "^3.0.1"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/longest-streak": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.72.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.72.0.tgz",
+ "integrity": "sha512-fbF5i1GU8GODETa3Q7rXqrJrcx3iPYrQtxthom103zGx+Z0BGwKYAW9s+9435ohYJ7hQm0av0PoZcqI1CRwNrg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.5.tgz",
+ "integrity": "sha512-oLLC2+YvQUfAa+vxoufE/yc1icb87M9iXIRqAoR/2EhE/8cb3SF/+Gi0c2By1B+5Vz2e2vQtUvHiu1+63Znh+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz",
+ "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz",
+ "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark": "^4.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-expression": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdx-jsx": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.1.tgz",
+ "integrity": "sha512-oXp1GIa+E9IwIs3a106+tr3Bz2MlTTBvlXjOZMoIat11kk7vjqgmBnksB9WHmWv+gZh7k3IUgpAHlwHB7DH4GQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "ccount": "^2.0.0",
+ "devlop": "^1.1.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0",
+ "parse-entities": "^4.0.0",
+ "stringify-entities": "^4.0.0",
+ "unist-util-stringify-position": "^4.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-mdxjs-esm": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree-jsx": "^1.0.0",
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "mdast-util-to-markdown": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-phrasing": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-hast": {
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
+ "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "trim-lines": "^3.0.0",
+ "unist-util-position": "^5.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-markdown": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.3.tgz",
+ "integrity": "sha512-dImdB6VZRMRac1SwAW6o1c0ESk/7pFFAMCMuKM+9v7LpTSthMM5nzXpqKw4Mjo66aXVKhNkyrP4bMB9MurPyWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "@types/unist": "^3.0.0",
+ "longest-streak": "^3.0.0",
+ "mdast-util-phrasing": "^4.0.0",
+ "mdast-util-to-string": "^4.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-decode-string": "^2.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "unist-util-visit": "^5.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "license": "MIT"
+ },
+ "node_modules/micromark": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz",
+ "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-core-commonmark": "^2.0.0",
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-combine-extensions": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-edit-map": "^1.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-sanitize-uri": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz",
+ "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "devlop": "^1.0.0",
+ "micromark-factory-destination": "^2.0.0",
+ "micromark-factory-label": "^2.0.0",
+ "micromark-factory-space": "^2.1.0",
+ "micromark-factory-title": "^2.0.0",
+ "micromark-factory-whitespace": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-classify-character": "^2.0.0",
+ "micromark-util-edit-map": "^1.0.0",
+ "micromark-util-html-tag-name": "^2.0.0",
+ "micromark-util-normalize-identifier": "^2.0.0",
+ "micromark-util-resolve-all": "^2.0.0",
+ "micromark-util-subtokenize": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.3"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz",
+ "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-factory-space": "^2.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-edit-map": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz",
+ "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "micromark-util-character": "^2.0.0",
+ "micromark-util-encode": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "devlop": "^1.0.0",
+ "micromark-util-chunked": "^2.0.0",
+ "micromark-util-symbol": "^2.0.0",
+ "micromark-util-types": "^2.0.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromark-util-types": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz",
+ "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mnemonist": {
+ "version": "0.39.8",
+ "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz",
+ "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "obliterator": "^2.0.1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.63",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz",
+ "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/obliterator": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz",
+ "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==",
+ "license": "MIT"
+ },
+ "node_modules/obug": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz",
+ "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/octokit": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/octokit/-/octokit-5.0.5.tgz",
+ "integrity": "sha512-4+/OFSqOjoyULo7eN7EA97DE0Xydj/PW5aIckxqQIoFjFwqXKuFCvXUJObyJfBF9Khu4RL/jlDRI9FPaMGfPnw==",
+ "license": "MIT",
+ "dependencies": {
+ "@octokit/app": "^16.1.2",
+ "@octokit/core": "^7.0.6",
+ "@octokit/oauth-app": "^8.0.3",
+ "@octokit/plugin-paginate-graphql": "^6.0.0",
+ "@octokit/plugin-paginate-rest": "^14.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "^17.0.0",
+ "@octokit/plugin-retry": "^8.0.3",
+ "@octokit/plugin-throttling": "^11.0.3",
+ "@octokit/request-error": "^7.0.2",
+ "@octokit/types": "^16.0.0",
+ "@octokit/webhooks": "^14.0.0"
+ },
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/on-exit-leak-free": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+ "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate/node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pandemonium": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/pandemonium/-/pandemonium-2.4.1.tgz",
+ "integrity": "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g==",
+ "license": "MIT",
+ "dependencies": {
+ "mnemonist": "^0.39.2"
+ }
+ },
+ "node_modules/parse-entities": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^2.0.0",
+ "character-entities-legacy": "^3.0.0",
+ "character-reference-invalid": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "is-alphanumerical": "^2.0.0",
+ "is-decimal": "^2.0.0",
+ "is-hexadecimal": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/parse-entities/node_modules/@types/unist": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
+ },
+ "node_modules/parse5": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.1.1.tgz",
+ "integrity": "sha512-EcR/6WDcU7b2N3vQSxKALCk9lXGtUMQxXio008OEMBE0kavu/7JLfBEeFPzi43nMqVDSbEn3QCK+BMCc9zO//w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^9.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5-parser-stream": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-8.1.1.tgz",
+ "integrity": "sha512-hMcVbbUJcQmP8rURWUdYrT1flRiBiBVjq/RYGguJpDUgIXSqdYKlE0CxthmdMWoSlBAuL5bUBa73g0pttSy7Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parse5": "^8.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz",
+ "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "11.5.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pino": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/pino/-/pino-10.4.0.tgz",
+ "integrity": "sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==",
+ "license": "MIT",
+ "dependencies": {
+ "@pinojs/redact": "^0.4.0",
+ "atomic-sleep": "^1.0.0",
+ "on-exit-leak-free": "^2.1.0",
+ "pino-abstract-transport": "^3.0.0",
+ "pino-std-serializers": "^7.0.0",
+ "process-warning": "^5.0.0",
+ "quick-format-unescaped": "^4.0.3",
+ "real-require": "^1.0.0",
+ "safe-stable-stringify": "^2.3.1",
+ "sonic-boom": "^4.0.1",
+ "thread-stream": "^4.0.0"
+ },
+ "bin": {
+ "pino": "bin.js"
+ }
+ },
+ "node_modules/pino-abstract-transport": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz",
+ "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.0.0"
+ }
+ },
+ "node_modules/pino-std-serializers": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
+ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
+ "license": "MIT"
+ },
+ "node_modules/playwright": {
+ "version": "1.65.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz",
+ "integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.65.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.65.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz",
+ "integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.32",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
+ "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.20",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.10.0.tgz",
+ "integrity": "sha512-mBi4765LgjO1PFjlEs7pUeK0nb1bw71c+N4/ItfeUWNdY5cV2pRKaFpbbFRYmDJMO+5ZwzGxHRqHxFZna7iv5w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/process-warning": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz",
+ "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/promise-worker-transferable": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "is-promise": "^2.1.0",
+ "lie": "^3.0.2"
+ }
+ },
+ "node_modules/property-information": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz",
+ "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qified": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+ "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hookified": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/qified/node_modules/hookified": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+ "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/quick-format-unescaped": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+ "license": "MIT"
+ },
+ "node_modules/radix-ui": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.8.1.tgz",
+ "integrity": "sha512-HPfpVGizO9xANlsFSauruLhRe/3+iOt3vPX0MXYle/UQi3gorQrSYtBcdiv0BZYewcl2rvL59qrIuLFMGAmUrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.2.0",
+ "@radix-ui/react-accessible-icon": "1.2.0",
+ "@radix-ui/react-accordion": "1.3.1",
+ "@radix-ui/react-alert-dialog": "1.2.1",
+ "@radix-ui/react-arrow": "1.1.17",
+ "@radix-ui/react-aspect-ratio": "1.2.0",
+ "@radix-ui/react-avatar": "1.3.1",
+ "@radix-ui/react-checkbox": "1.4.1",
+ "@radix-ui/react-collapsible": "1.2.1",
+ "@radix-ui/react-collection": "1.2.0",
+ "@radix-ui/react-compose-refs": "1.1.5",
+ "@radix-ui/react-context": "1.3.0",
+ "@radix-ui/react-context-menu": "2.4.1",
+ "@radix-ui/react-dialog": "1.3.1",
+ "@radix-ui/react-direction": "1.2.0",
+ "@radix-ui/react-dismissable-layer": "1.2.1",
+ "@radix-ui/react-dropdown-menu": "2.2.1",
+ "@radix-ui/react-focus-guards": "1.2.0",
+ "@radix-ui/react-focus-scope": "1.3.1",
+ "@radix-ui/react-form": "0.3.0",
+ "@radix-ui/react-hover-card": "1.2.1",
+ "@radix-ui/react-label": "2.1.17",
+ "@radix-ui/react-menu": "2.2.1",
+ "@radix-ui/react-menubar": "1.2.1",
+ "@radix-ui/react-navigation-menu": "1.4.1",
+ "@radix-ui/react-one-time-password-field": "0.2.1",
+ "@radix-ui/react-password-toggle-field": "0.2.1",
+ "@radix-ui/react-popover": "1.3.1",
+ "@radix-ui/react-popper": "1.4.0",
+ "@radix-ui/react-portal": "1.2.0",
+ "@radix-ui/react-presence": "1.1.11",
+ "@radix-ui/react-primitive": "2.2.0",
+ "@radix-ui/react-progress": "1.2.1",
+ "@radix-ui/react-radio-group": "1.5.1",
+ "@radix-ui/react-roving-focus": "1.2.1",
+ "@radix-ui/react-scroll-area": "1.4.0",
+ "@radix-ui/react-select": "2.4.1",
+ "@radix-ui/react-separator": "1.2.0",
+ "@radix-ui/react-slider": "1.6.1",
+ "@radix-ui/react-slot": "1.5.0",
+ "@radix-ui/react-switch": "1.4.1",
+ "@radix-ui/react-tabs": "1.2.1",
+ "@radix-ui/react-toast": "1.3.1",
+ "@radix-ui/react-toggle": "1.2.1",
+ "@radix-ui/react-toggle-group": "1.2.1",
+ "@radix-ui/react-toolbar": "1.2.1",
+ "@radix-ui/react-tooltip": "1.4.1",
+ "@radix-ui/react-use-callback-ref": "1.1.5",
+ "@radix-ui/react-use-controllable-state": "1.3.0",
+ "@radix-ui/react-use-effect-event": "0.0.5",
+ "@radix-ui/react-use-escape-keydown": "1.1.6",
+ "@radix-ui/react-use-is-hydrated": "0.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.4",
+ "@radix-ui/react-use-size": "1.1.5",
+ "@radix-ui/react-visually-hidden": "1.2.13"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react": {
+ "version": "19.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
+ "integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz",
+ "integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.28.0"
+ },
+ "peerDependencies": {
+ "react": "^19.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-markdown": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
+ "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "devlop": "^1.0.0",
+ "hast-util-to-jsx-runtime": "^2.0.0",
+ "html-url-attributes": "^3.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "remark-parse": "^11.0.0",
+ "remark-rehype": "^11.0.0",
+ "unified": "^11.0.0",
+ "unist-util-visit": "^5.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18",
+ "react": ">=18"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.5.1.tgz",
+ "integrity": "sha512-5u1eQ46QjuXjNfEnLwzrnou1Bm5dquO3xjVYkkMYCudtkicD2AQHtx+fvXzPATtPJx8TW6DR1x1TDRqsfHyZBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/route-pattern": "^0.22.1",
+ "cookie-es": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=22.22.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=19.2.0",
+ "@types/react-dom": ">=19.2.0",
+ "react": ">=19.2.7",
+ "react-dom": ">=19.2.7"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/real-require": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz",
+ "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
+ "license": "MIT"
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/remark-parse": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mdast": "^4.0.0",
+ "mdast-util-from-markdown": "^2.0.0",
+ "micromark-util-types": "^2.0.0",
+ "unified": "^11.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/remark-rehype": {
+ "version": "11.1.2",
+ "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
+ "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@types/mdast": "^4.0.0",
+ "mdast-util-to-hast": "^13.0.0",
+ "unified": "^11.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ret": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.1.tgz",
+ "integrity": "sha512-Z/nyvABwcRnRD2mMP7Kz6G1dFZZqX1mNErOnxF42OCQKE07b9k9hK9SQxHL3LFEZUdzQaLM4dId1E/E6Vm4WhA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.19",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.19.tgz",
+ "integrity": "sha512-QIiV1J9d0pzN4jeeTH4sjPy0AZ6cAy2cG0ExeoE1+U8Rvj+hR3gYkokx0YbeUNGlZqiQJJ2utcETe0oKXwzsuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.156.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.19",
+ "@rolldown/binding-android-arm64": "1.2.19",
+ "@rolldown/binding-darwin-arm64": "1.2.19",
+ "@rolldown/binding-darwin-x64": "1.2.19",
+ "@rolldown/binding-freebsd-x64": "1.2.19",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.19",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.19",
+ "@rolldown/binding-linux-arm64-musl": "1.2.19",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.19",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.19",
+ "@rolldown/binding-linux-x64-gnu": "1.2.19",
+ "@rolldown/binding-linux-x64-musl": "1.2.19",
+ "@rolldown/binding-openharmony-arm64": "1.2.19",
+ "@rolldown/binding-wasm32-wasi": "1.2.19",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.19",
+ "@rolldown/binding-win32-x64-msvc": "1.2.19"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-regex2": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz",
+ "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "ret": "~0.5.0"
+ },
+ "bin": {
+ "safe-regex2": "bin/safe-regex2.js"
+ }
+ },
+ "node_modules/safe-stable-stringify": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+ "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.28.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
+ "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
+ "license": "MIT"
+ },
+ "node_modules/secure-json-parse": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.1.tgz",
+ "integrity": "sha512-lnd81MQEZKjhW/Gd2TQqdd4XP6QYNdDW/EP+I7nTrFHP9y2LmaGPIHk9xsnzcOiEr1c39c/e84xFgGviQKBTRw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/semver": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz",
+ "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.2.2.tgz",
+ "integrity": "sha512-df017qMeYuXo4emL09nlyMpi2i2MtEIKS/6jtXH3vmjFgIRsOi852/5RmhJ4EkvN3ZF8jXadM/EkTPXgc8yPIw==",
+ "license": "MIT"
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sonic-boom": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
+ "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "atomic-sleep": "^1.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/space-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stringify-entities": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
+ "license": "MIT",
+ "dependencies": {
+ "character-entities-html4": "^2.0.0",
+ "character-entities-legacy": "^3.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/style-mod": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz",
+ "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==",
+ "license": "MIT"
+ },
+ "node_modules/style-to-object": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-2.0.2.tgz",
+ "integrity": "sha512-QmbXPUylqU80HAgTOJeEOdbA40W9KtxmjMhQy+tq+b8F2aoh3aQLuy82hh2jm2qsC9hR9eI0+eQidTMydKcqRA==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.9"
+ }
+ },
+ "node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.5.tgz",
+ "integrity": "sha512-FcQk9BuffmEHJP2G0XmB2SplU1aLTI7j07Kunk+U74Ny+XiWOtzXz9aujFZ/avntw1e2cOv/3PkgzflQ7gX2vQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/thread-stream": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.1.tgz",
+ "integrity": "sha512-Stg6edJXC/6/xqGwSXwFokrUhChJ18amEjpKH89WrQQ0oVFDF+pUUcTpiwWhFUrE89Vf16FBOlwhaw+lescxMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "real-require": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.187.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.187.0.tgz",
+ "integrity": "sha512-Qiu7R+Cmh8CstZsY1uJOSWnVlfo7ZtNLV7mF8Jltk3LB3AtDu9cRZlcnRKkAk8Y4z6KKw35z7Y7Urd7jWY8etA==",
+ "license": "MIT"
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
+ "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.11",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.11.tgz",
+ "integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.3.tgz",
+ "integrity": "sha512-Al01C2JIBnO2CXCAPbpAl6sUiOZHeThGtHYNTa51JRw9PyWOCdr+9Ong05R4qg/Sk3+H645E8ov1h2c58ityNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz",
+ "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "7.4.22",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.22.tgz",
+ "integrity": "sha512-TmLwkCZ1GFMxGkKfg2ooQhPF8rDMuuXvJHSxFiOU1fofh27L6NaO5T9KSlQXCJX2eoSt14I8F4saSMILOmXmaQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.22"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.22",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.22.tgz",
+ "integrity": "sha512-NRyhrlvJcTwvoRibE0dEZvp6C5RwV4RxpsX1zbkI1k7Fk2pzIhmQ+MvswJEd5KKl3VRFRMDTipXse/ejh/ld/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toad-cache": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
+ "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.1.0.tgz",
+ "integrity": "sha512-FTxc8u8Kl2tz0Lat1u8ksvcy8bwHRyEMawwTWybo51JqaC6O7SvfuhAKg+Ld6VqyIe2QRJBzY9HA7y2WyFRT2w==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-7.0.0.tgz",
+ "integrity": "sha512-BcDSDW+SIGWVOns1Zm6Yg91q9AEj7eaID0QDY9oIqJD1A8dscNwMcl9Sms2DeuWCNov1X0/oXanHGYQBSLwZsg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": "^22.14.0 || >=24.0.0"
+ }
+ },
+ "node_modules/trim-lines": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
+ "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/troika-three-text": {
+ "version": "0.52.5",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.5.tgz",
+ "integrity": "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.52.5",
+ "troika-worker-utils": "^0.52.0",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.52.5",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.5.tgz",
+ "integrity": "sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
+ "license": "MIT"
+ },
+ "node_modules/trough": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
+ "integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
+ "license": "0BSD"
+ },
+ "node_modules/tsx": {
+ "version": "4.23.15",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.28.0"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.72.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.2.tgz",
+ "integrity": "sha512-c/+AKMW1KB81r28Sp0AAdcyXj5dzNmAsZFcsrxKbZTKmcBK2xIwBR31dcSYSDu3bKmzXe8GKrSxO+eDQ401Uzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.72.2",
+ "@typescript-eslint/parser": "8.72.2",
+ "@typescript-eslint/typescript-estree": "8.72.2",
+ "@typescript-eslint/utils": "8.72.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici": {
+ "version": "8.12.4",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.4.tgz",
+ "integrity": "sha512-8Mx+uNKKER2bA4AdtedkqRbwUCIJFr2nJckXRe5yrDqzePqwLP7VNcctN8/XdmDSbF7mS+jJKIszlL+vbam1VA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.19.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.24.6",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
+ "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/unified": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "bail": "^2.0.0",
+ "devlop": "^1.0.0",
+ "extend": "^3.0.0",
+ "is-plain-obj": "^4.0.0",
+ "trough": "^2.0.0",
+ "vfile": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-is": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
+ "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-position": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
+ "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.2.0.tgz",
+ "integrity": "sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0",
+ "unist-util-visit-parents": "^6.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/unist-util-visit-parents": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.1.0.tgz",
+ "integrity": "sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-is": "^6.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/universal-github-app-jwt": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-2.2.2.tgz",
+ "integrity": "sha512-dcmbeSrOdTnsjGjUfAlqNDJrhxXizjAz94ija9Qw8YkZ1uu0d+GoZzyH+Jb9tIIqvGsadUfwg+22k5aDqqwzbw==",
+ "license": "MIT"
+ },
+ "node_modules/universal-user-agent": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
+ "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
+ "license": "ISC"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz",
+ "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
+ "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/vfile": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "vfile-message": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vfile-message": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.4.tgz",
+ "integrity": "sha512-66ycRswIkNmHH9PHku7ADtv5RVwgIrOcuv3J8Svz6h4qgGRrKVa+9/neuKyJZUXyOpYwLTvS+whZ4R2urHHgQg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/unist": "^3.0.0",
+ "unist-util-stringify-position": "^4.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.3.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.8.tgz",
+ "integrity": "sha512-LzOn3h9rG/0vhLdXrV4yJ4QFA9eEoqCl+Yx+kGJd7aB9w6nfNgVHKSYwLPZUHrz4ZGa+t+kXjV9itvcHDLQouA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.32",
+ "rolldown": "~1.2.18",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.7.1 || ^0.8.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.1.0.tgz",
+ "integrity": "sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.1.0",
+ "chai": "^6.3.0",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.4.3",
+ "obug": "^3.0.0",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.3.0",
+ "tinybench": "^6.2.2",
+ "tinyexec": "^1.3.1",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "3.2.1"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.1.0",
+ "@vitest/browser-preview": "5.1.0",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.1.0",
+ "@vitest/coverage-v8": "5.1.0",
+ "@vitest/ui": "5.1.0",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT"
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz",
+ "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
+ }
+ },
+ "node_modules/web-tree-sitter": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.27.2.tgz",
+ "integrity": "sha512-ZRo86GKI4aOSBAqOrPmCtriExI3xIzu1sNw/V6v3b4QGxrEmO19FGwbMNdyhw3MJLAZIw0xAV0oSpqX361eAyQ==",
+ "license": "MIT"
+ },
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "17.3.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.3.0.tgz",
+ "integrity": "sha512-KwOAJrpf+GTgBvcBQJcP38sMF0DsQKdwtG+TxM2pW44P6E5k0hDbWrDmyuJDIF0Uef0aQq0ZoJs+5oI+k8ddhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.16.0",
+ "tr46": "^7.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^22.14.0 || >=24.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
+ "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.11"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yauzl": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz",
+ "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==",
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.6.7",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
+ "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.1.0.tgz",
+ "integrity": "sha512-ciJZlP48M1PTQbVQq81CGVFxHTLpZNrFjKqwjtCU34AWuXTZT/jrF3p9+fI84Cf1eSCMLapxqBy/SNTfwOBuqA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "packages/graph-engine": {
+ "name": "@vectora/graph-engine",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@vectora/graph-schema": "*",
+ "@vectora/shared": "*",
+ "graphology": "^0.26.0",
+ "graphology-communities-louvain": "^2.0.2",
+ "graphology-types": "^0.24.8"
+ }
+ },
+ "packages/graph-layout": {
+ "name": "@vectora/graph-layout",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@vectora/graph-schema": "*",
+ "d3-force-3d": "^3.0.6"
+ }
+ },
+ "packages/graph-schema": {
+ "name": "@vectora/graph-schema",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.6.7"
+ }
+ },
+ "packages/language-adapters": {
+ "name": "@vectora/language-adapters",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@vectora/graph-schema": "*",
+ "typescript": "~5.9.3",
+ "web-tree-sitter": "0.27.2"
+ }
+ },
+ "packages/repository-indexer": {
+ "name": "@vectora/repository-indexer",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@vectora/graph-engine": "*",
+ "@vectora/graph-schema": "*",
+ "@vectora/language-adapters": "*",
+ "@vectora/shared": "*",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.14"
+ }
+ },
+ "packages/repository-indexer/node_modules/ignore": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz",
+ "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "packages/search": {
+ "name": "@vectora/search",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@vectora/graph-schema": "*"
+ }
+ },
+ "packages/shared": {
+ "name": "@vectora/shared",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.6.7"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..76437bc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "vectora",
+ "version": "0.1.0",
+ "private": true,
+ "description": "VECTORA — codebase intelligence platform. Understand what connects.",
+ "license": "MIT",
+ "type": "module",
+ "engines": {
+ "node": ">=22.13.0"
+ },
+ "workspaces": [
+ "packages/*",
+ "apps/*"
+ ],
+ "scripts": {
+ "dev": "node scripts/dev.mjs",
+ "dev:engine": "npm run dev -w @vectora/local-engine",
+ "dev:web": "npm run dev -w @vectora/web",
+ "build": "node scripts/build.mjs",
+ "start": "node dist/engine/server.mjs",
+ "typecheck": "tsc --noEmit -p tsconfig.json",
+ "lint": "eslint .",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:coverage": "vitest run --coverage",
+ "test:e2e": "playwright test",
+ "bench": "tsx scripts/bench.ts",
+ "licenses": "node scripts/license-inventory.mjs"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.0",
+ "@playwright/test": "^1.65.0",
+ "@testing-library/jest-dom": "^6.6.0",
+ "@testing-library/react": "^16.3.3",
+ "@types/node": "^24.0.0",
+ "@vitest/coverage-v8": "^5.1.0",
+ "esbuild": "^0.28.0",
+ "eslint": "^10.14.0",
+ "eslint-plugin-react-hooks": "^7.0.0",
+ "globals": "^16.0.0",
+ "jsdom": "^31.0.0",
+ "prettier": "^3.10.0",
+ "tsx": "^4.23.15",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.72.2",
+ "vitest": "^5.1.0"
+ }
+}
diff --git a/packages/graph-engine/package.json b/packages/graph-engine/package.json
new file mode 100644
index 0000000..3cbe0cd
--- /dev/null
+++ b/packages/graph-engine/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@vectora/graph-engine",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "dependencies": {
+ "@vectora/graph-schema": "*",
+ "@vectora/shared": "*",
+ "graphology": "^0.26.0",
+ "graphology-communities-louvain": "^2.0.2",
+ "graphology-types": "^0.24.8"
+ }
+}
diff --git a/packages/graph-engine/src/algorithms.ts b/packages/graph-engine/src/algorithms.ts
new file mode 100644
index 0000000..a0800cc
--- /dev/null
+++ b/packages/graph-engine/src/algorithms.ts
@@ -0,0 +1,300 @@
+/**
+ * Graph algorithms over the knowledge graph: adjacency index, traversal,
+ * cycle detection, impact analysis and community detection.
+ */
+import Graph from 'graphology';
+import louvain from 'graphology-communities-louvain';
+import type { EdgeType, GraphEdge, GraphNode, KnowledgeGraph } from '@vectora/graph-schema';
+
+export interface GraphIndex {
+ nodes: Map;
+ edges: Map;
+ out: Map;
+ in: Map;
+}
+
+export function buildIndex(graph: Pick): GraphIndex {
+ const nodes = new Map();
+ const edges = new Map();
+ const out = new Map();
+ const inn = new Map();
+ for (const n of graph.nodes) nodes.set(n.id, n);
+ for (const e of graph.edges) {
+ edges.set(e.id, e);
+ (out.get(e.source) ?? out.set(e.source, []).get(e.source)!).push(e);
+ (inn.get(e.target) ?? inn.set(e.target, []).get(e.target)!).push(e);
+ }
+ return { nodes, edges, out, in: inn };
+}
+
+/** Relationship types that express "A needs B to work". */
+export const DEPENDENCY_EDGES: readonly EdgeType[] = [
+ 'IMPORTS',
+ 'CALLS',
+ 'USES',
+ 'EXTENDS',
+ 'IMPLEMENTS',
+ 'INSTANTIATES',
+ 'READS_FROM',
+ 'WRITES_TO',
+ 'ROUTES_TO',
+ 'DEPENDS_ON',
+];
+const HIERARCHY_EDGES: readonly EdgeType[] = ['CONTAINS', 'DEFINED_IN', 'BELONGS_TO', 'EXPORTS'];
+
+export interface TraversalResult {
+ /** node id -> distance from the start */
+ distances: Map;
+ edges: GraphEdge[];
+ truncated: boolean;
+}
+
+export function traverse(
+ index: GraphIndex,
+ start: string,
+ direction: 'out' | 'in',
+ opts: { types?: readonly EdgeType[]; maxDepth?: number; maxNodes?: number } = {},
+): TraversalResult {
+ const types = new Set(opts.types ?? DEPENDENCY_EDGES);
+ const maxDepth = opts.maxDepth ?? 4;
+ const maxNodes = opts.maxNodes ?? 500;
+ const distances = new Map([[start, 0]]);
+ const edges: GraphEdge[] = [];
+ let frontier = [start];
+ let truncated = false;
+ for (let d = 1; d <= maxDepth && frontier.length; d++) {
+ const next: string[] = [];
+ for (const id of frontier) {
+ for (const e of (direction === 'out' ? index.out : index.in).get(id) ?? []) {
+ if (!types.has(e.type)) continue;
+ const other = direction === 'out' ? e.target : e.source;
+ edges.push(e);
+ if (distances.has(other)) continue;
+ if (distances.size >= maxNodes) {
+ truncated = true;
+ continue;
+ }
+ distances.set(other, d);
+ next.push(other);
+ }
+ }
+ frontier = next;
+ }
+ return { distances, edges, truncated };
+}
+
+/** Shortest directed path between two nodes over dependency edges. */
+export function shortestPath(
+ index: GraphIndex,
+ from: string,
+ to: string,
+ types: readonly EdgeType[] = DEPENDENCY_EDGES,
+ maxDepth = 12,
+): GraphEdge[] | null {
+ if (from === to) return [];
+ const allowed = new Set(types);
+ const prev = new Map();
+ const seen = new Set([from]);
+ let frontier = [from];
+ for (let d = 0; d < maxDepth && frontier.length; d++) {
+ const next: string[] = [];
+ for (const id of frontier) {
+ for (const e of index.out.get(id) ?? []) {
+ if (!allowed.has(e.type) || seen.has(e.target)) continue;
+ seen.add(e.target);
+ prev.set(e.target, e);
+ if (e.target === to) {
+ const path: GraphEdge[] = [];
+ for (let cur = to; cur !== from;) {
+ const pe = prev.get(cur)!;
+ path.unshift(pe);
+ cur = pe.source;
+ }
+ return path;
+ }
+ next.push(e.target);
+ }
+ }
+ frontier = next;
+ }
+ return null;
+}
+
+/**
+ * Strongly connected components (Tarjan, iterative) over the given edge
+ * types. Components with more than one node are dependency cycles.
+ */
+export function findCycles(
+ index: GraphIndex,
+ types: readonly EdgeType[] = ['IMPORTS'],
+ nodeFilter?: (n: GraphNode) => boolean,
+): string[][] {
+ const allowed = new Set(types);
+ const ids = [...index.nodes.values()].filter((n) => !nodeFilter || nodeFilter(n)).map((n) => n.id);
+ const idx = new Map();
+ const low = new Map();
+ const onStack = new Set();
+ const stack: string[] = [];
+ const result: string[][] = [];
+ let counter = 0;
+ const succ = (id: string): string[] => {
+ const out: string[] = [];
+ for (const e of index.out.get(id) ?? []) {
+ if (!allowed.has(e.type) || e.target === id) continue;
+ const t = index.nodes.get(e.target);
+ if (t && (!nodeFilter || nodeFilter(t))) out.push(e.target);
+ }
+ return out;
+ };
+ for (const root of ids) {
+ if (idx.has(root)) continue;
+ const work: { id: string; next: string[]; i: number }[] = [{ id: root, next: succ(root), i: 0 }];
+ idx.set(root, counter);
+ low.set(root, counter++);
+ stack.push(root);
+ onStack.add(root);
+ while (work.length) {
+ const frame = work[work.length - 1]!;
+ if (frame.i < frame.next.length) {
+ const w = frame.next[frame.i++]!;
+ if (!idx.has(w)) {
+ idx.set(w, counter);
+ low.set(w, counter++);
+ stack.push(w);
+ onStack.add(w);
+ work.push({ id: w, next: succ(w), i: 0 });
+ } else if (onStack.has(w)) low.set(frame.id, Math.min(low.get(frame.id)!, idx.get(w)!));
+ } else {
+ work.pop();
+ const parent = work[work.length - 1];
+ if (parent) low.set(parent.id, Math.min(low.get(parent.id)!, low.get(frame.id)!));
+ if (low.get(frame.id) === idx.get(frame.id)) {
+ const comp: string[] = [];
+ let w: string;
+ do {
+ w = stack.pop()!;
+ onStack.delete(w);
+ comp.push(w);
+ } while (w !== frame.id);
+ if (comp.length > 1) result.push(comp.reverse());
+ }
+ }
+ }
+ }
+ return result.sort((a, b) => b.length - a.length);
+}
+
+/** One concrete cycle path inside a strongly connected component. */
+export function cyclePath(
+ index: GraphIndex,
+ component: string[],
+ types: readonly EdgeType[] = ['IMPORTS'],
+): GraphEdge[] {
+ const set = new Set(component);
+ const start = component[0]!;
+ const allowed = new Set(types);
+ const prev = new Map();
+ let frontier = [start];
+ const seen = new Set();
+ while (frontier.length) {
+ const next: string[] = [];
+ for (const id of frontier) {
+ for (const e of index.out.get(id) ?? []) {
+ if (!allowed.has(e.type) || !set.has(e.target)) continue;
+ if (e.target === start) {
+ const path = [e];
+ for (let cur = id; cur !== start;) {
+ const pe = prev.get(cur)!;
+ path.unshift(pe);
+ cur = pe.source;
+ }
+ return path;
+ }
+ if (seen.has(e.target)) continue;
+ seen.add(e.target);
+ prev.set(e.target, e);
+ next.push(e.target);
+ }
+ }
+ frontier = next;
+ }
+ return [];
+}
+
+/** Everything that directly or transitively depends on `id` (reverse reachability). */
+export function impactOf(index: GraphIndex, id: string, maxDepth = 5, maxNodes = 400): TraversalResult {
+ // A change to a container affects dependants of its members too.
+ const seeds = [id];
+ for (const e of index.out.get(id) ?? []) if (e.type === 'CONTAINS') seeds.push(e.target);
+ const merged: TraversalResult = { distances: new Map([[id, 0]]), edges: [], truncated: false };
+ const edgeSeen = new Set();
+ for (const s of seeds) {
+ const r = traverse(index, s, 'in', { maxDepth, maxNodes });
+ for (const [k, v] of r.distances)
+ if (!merged.distances.has(k) || merged.distances.get(k)! > v) merged.distances.set(k, v);
+ for (const e of r.edges) {
+ if (edgeSeen.has(e.id)) continue;
+ edgeSeen.add(e.id);
+ merged.edges.push(e);
+ }
+ merged.truncated ||= r.truncated;
+ }
+ return merged;
+}
+
+/** The file (or the node itself) that owns a symbol. */
+export function containerChain(index: GraphIndex, id: string): GraphNode[] {
+ const chain: GraphNode[] = [];
+ let cur: string | undefined = id;
+ const seen = new Set();
+ while (cur && !seen.has(cur)) {
+ seen.add(cur);
+ const parent: GraphEdge | undefined = (index.in.get(cur) ?? []).find(
+ (e) => e.type === 'CONTAINS' && e.extractionMethod === 'filesystem',
+ );
+ if (!parent) break;
+ const n = index.nodes.get(parent.source);
+ if (n) chain.unshift(n);
+ cur = parent.source;
+ }
+ return chain;
+}
+
+/**
+ * Louvain community detection on the undirected projection of dependency
+ * edges. Writes `metadata.community` on each node and returns the count.
+ */
+export function assignCommunities(nodes: GraphNode[], edges: GraphEdge[]): number {
+ const g = new Graph({ type: 'undirected', multi: false, allowSelfLoops: false });
+ for (const n of nodes) g.addNode(n.id);
+ const skip = new Set(HIERARCHY_EDGES.filter((t) => t !== 'CONTAINS'));
+ for (const e of edges) {
+ if (skip.has(e.type) || e.source === e.target) continue;
+ if (!g.hasNode(e.source) || !g.hasNode(e.target)) continue;
+ // Containment keeps symbols near their file; dependencies pull related files together.
+ const w = e.type === 'CONTAINS' ? 1.5 : 1;
+ if (g.hasEdge(e.source, e.target))
+ g.updateEdgeAttribute(e.source, e.target, 'weight', (x: number | undefined) => (x ?? 0) + w);
+ else g.addEdge(e.source, e.target, { weight: w });
+ }
+ if (g.size === 0) {
+ for (const n of nodes) n.metadata.community = 0;
+ return nodes.length ? 1 : 0;
+ }
+ // A fixed seed keeps community ids stable between runs on the same graph.
+ let seed = 42;
+ const rng = () => {
+ seed = (seed * 1664525 + 1013904223) % 4294967296;
+ return seed / 4294967296;
+ };
+ const communities = louvain(g, { getEdgeWeight: 'weight', rng, resolution: 1 });
+ const remap = new Map();
+ for (const n of nodes) {
+ const c = communities[n.id] ?? -1;
+ if (!remap.has(c)) remap.set(c, remap.size);
+ n.metadata.community = remap.get(c)!;
+ }
+ return remap.size;
+}
+
+export { HIERARCHY_EDGES };
diff --git a/packages/graph-engine/src/analysis.ts b/packages/graph-engine/src/analysis.ts
new file mode 100644
index 0000000..191b3d6
--- /dev/null
+++ b/packages/graph-engine/src/analysis.ts
@@ -0,0 +1,244 @@
+/**
+ * Repository analysis record (persisted next to the graph) and the overview
+ * derived from it. Every summary statement carries the evidence it is based on.
+ */
+import {
+ type GraphNode,
+ type KnowledgeGraph,
+ type UnresolvedReference,
+ type ValidationIssue,
+ isSymbolType,
+} from '@vectora/graph-schema';
+import type { IndexSummary, RepositoryOverview, RepositoryRecord } from '@vectora/shared';
+import { type GraphIndex, buildIndex, findCycles } from './algorithms';
+
+export interface ExecutionFlow {
+ id: string;
+ name: string;
+ kind: 'request' | 'startup';
+ steps: string[];
+ reachesData: boolean;
+}
+
+export interface RepositoryAnalysis {
+ generatedAt: string;
+ /** Hash over every indexed file's path and content hash plus the options that shape the result. */
+ fingerprint?: string;
+ description: string | null;
+ descriptionSource: string | null;
+ languages: {
+ language: string;
+ name: string;
+ tier: number;
+ files: number;
+ analyzedFiles: number;
+ symbols: number;
+ parseErrors: number;
+ adapter: string | null;
+ lines: number;
+ }[];
+ frameworks: { name: string; evidence: string }[];
+ entryPoints: { path: string; nodeId: string; reason: string }[];
+ flows: ExecutionFlow[];
+ declaredDependencies: {
+ name: string;
+ version: string | null;
+ manifest: string;
+ ecosystem: string;
+ dev: boolean;
+ line: number;
+ }[];
+ unresolvedCount: number;
+ unresolvedByReason: Record;
+ unresolvedSamples: UnresolvedReference[];
+ validation: { errors: number; warnings: number; issues: ValidationIssue[] };
+ normalization: { removedNodes: number; mergedEdges: number; droppedEdges: number };
+ skipped: Record;
+ truncated: boolean;
+ parseIssues: { path: string; message: string; line?: number; severity: string }[];
+ communities: number;
+ summary: IndexSummary;
+}
+
+export function buildOverview(
+ repository: RepositoryRecord,
+ graph: KnowledgeGraph,
+ analysis: RepositoryAnalysis | null,
+ index?: GraphIndex,
+): RepositoryOverview {
+ const idx = index ?? buildIndex(graph);
+ const fileNodes = graph.nodes.filter((n) => n.type === 'File' || n.type === 'Configuration');
+
+ const symbolsPerFile = new Map();
+ for (const n of graph.nodes)
+ if (isSymbolType(n.type) && n.filePath)
+ symbolsPerFile.set(n.filePath, (symbolsPerFile.get(n.filePath) ?? 0) + 1);
+
+ const subsystems = graph.nodes
+ .filter((n) => n.type === 'Subsystem')
+ .map((s) => {
+ const files = (idx.in.get(s.id) ?? []).filter((e) => e.type === 'BELONGS_TO');
+ let symbols = 0;
+ for (const e of files) symbols += symbolsPerFile.get(idx.nodes.get(e.source)?.filePath ?? '') ?? 0;
+ const dep = (list: typeof graph.edges | undefined) =>
+ (list ?? [])
+ .filter((e) => e.type === 'DEPENDS_ON')
+ .reduce((a, e) => a + Number(e.metadata?.count ?? 1), 0);
+ return {
+ id: s.id,
+ name: s.name,
+ files: files.length,
+ symbols,
+ inbound: dep(idx.in.get(s.id)),
+ outbound: dep(idx.out.get(s.id)),
+ };
+ })
+ .sort((a, b) => b.files - a.files);
+
+ const concepts = graph.nodes
+ .filter((n) => n.type === 'Concept')
+ .map((c) => {
+ const members = (idx.in.get(c.id) ?? [])
+ .filter((e) => e.type === 'BELONGS_TO')
+ .sort((a, b) => b.confidence - a.confidence);
+ return {
+ id: c.id,
+ name: c.name,
+ members: members.length,
+ evidence: members
+ .slice(0, 3)
+ .map((e) => `${idx.nodes.get(e.source)?.filePath ?? e.source}: ${e.evidence}`),
+ };
+ })
+ .sort((a, b) => b.members - a.members);
+
+ const largestModules = fileNodes
+ .filter((f) => f.type === 'File')
+ .map((f) => ({
+ id: f.id,
+ path: f.filePath!,
+ symbols: symbolsPerFile.get(f.filePath!) ?? 0,
+ lines: Number(f.metadata.lines ?? 0),
+ }))
+ .sort((a, b) => b.symbols - a.symbols || b.lines - a.lines)
+ .slice(0, 12);
+
+ const packages = graph.nodes.filter((n) => n.type === 'Package');
+ const externalPackages = packages
+ .map((p) => ({
+ name: p.name,
+ importers: new Set((idx.in.get(p.id) ?? []).filter((e) => e.type === 'IMPORTS').map((e) => e.source))
+ .size,
+ declared: !!p.metadata.declared,
+ version: (p.metadata.version as string | null) ?? null,
+ }))
+ .sort((a, b) => b.importers - a.importers || (a.name < b.name ? -1 : 1));
+ const internalImports = graph.edges.filter(
+ (e) => e.type === 'IMPORTS' && !e.target.startsWith('pkg:'),
+ ).length;
+
+ // ---- architecture summary: statements with evidence ---------------------
+ const summary: RepositoryOverview['architectureSummary'] = [];
+ const langs = analysis?.languages ?? [];
+ const analysed = langs.filter((l) => l.tier > 0 && l.files > 0).sort((a, b) => b.files - a.files);
+ if (analysed.length) {
+ const top = analysed[0]!;
+ summary.push({
+ statement: `${fileNodes.length} files indexed; the primary language is ${top.name} (${top.files} files, ${top.symbols} symbols).`,
+ evidence: `File extensions and ${top.adapter ?? 'syntax'} analysis`,
+ verified: true,
+ });
+ }
+ if (subsystems.length > 1) {
+ summary.push({
+ statement: `The repository is organised into ${subsystems.length} subsystems; the largest are ${subsystems
+ .slice(0, 3)
+ .map((s) => `${s.name} (${s.files} files)`)
+ .join(', ')}.`,
+ evidence: 'Directory layout and workspace manifests',
+ verified: true,
+ });
+ const deps = graph.edges
+ .filter((e) => e.type === 'DEPENDS_ON' && e.source.startsWith('sub:'))
+ .sort((a, b) => Number(b.metadata?.count ?? 0) - Number(a.metadata?.count ?? 0));
+ for (const d of deps.slice(0, 4))
+ summary.push({
+ statement: `${idx.nodes.get(d.source)?.name} depends on ${idx.nodes.get(d.target)?.name} through ${d.metadata?.count} relationships.`,
+ evidence: d.evidence,
+ verified: true,
+ });
+ }
+ const endpoints = graph.nodes.filter((n) => n.type === 'ApiEndpoint');
+ if (endpoints.length)
+ summary.push({
+ statement: `${endpoints.length} API endpoint${endpoints.length === 1 ? ' is' : 's are'} defined, for example ${endpoints
+ .slice(0, 3)
+ .map((e) => e.name)
+ .join(', ')}.`,
+ evidence: endpoints
+ .slice(0, 3)
+ .map((e) => `${e.filePath}:${e.location?.startLine}`)
+ .join(', '),
+ verified: true,
+ });
+ const models = graph.nodes.filter((n) => n.type === 'DatabaseModel');
+ if (models.length)
+ summary.push({
+ statement: `${models.length} database model${models.length === 1 ? '' : 's'} detected: ${models
+ .slice(0, 5)
+ .map((m) => m.name)
+ .join(', ')}${models.length > 5 ? ', ...' : ''}.`,
+ evidence: models
+ .slice(0, 3)
+ .map((m) => `${m.filePath}:${m.location?.startLine}`)
+ .join(', '),
+ verified: true,
+ });
+ const dataFlows = (analysis?.flows ?? []).filter((f) => f.reachesData);
+ if (dataFlows.length)
+ summary.push({
+ statement: `${dataFlows.length} request flow${dataFlows.length === 1 ? '' : 's'} can be traced from an endpoint to data access.`,
+ evidence: `e.g. ${dataFlows[0]!.name}: ${dataFlows[0]!.steps.map((s) => idx.nodes.get(s)?.name ?? s).join(' → ')}`,
+ verified: false,
+ });
+ const cycles = findCycles(idx, ['IMPORTS'], (n: GraphNode) => n.type === 'File');
+ if (cycles.length)
+ summary.push({
+ statement: `${cycles.length} circular import group${cycles.length === 1 ? '' : 's'} found; the largest involves ${cycles[0]!.length} files.`,
+ evidence: cycles[0]!
+ .slice(0, 4)
+ .map((id) => idx.nodes.get(id)?.filePath)
+ .join(' → '),
+ verified: true,
+ });
+ if (analysis && analysis.unresolvedCount > 0)
+ summary.push({
+ statement: `${analysis.unresolvedCount} references could not be tied to a definition and are not shown as relationships.`,
+ evidence: Object.entries(analysis.unresolvedByReason)
+ .map(([r, c]) => `${c} × ${r}`)
+ .join('; '),
+ verified: true,
+ });
+
+ return {
+ repository,
+ description: analysis?.description ?? null,
+ descriptionSource: analysis?.descriptionSource ?? null,
+ languages: (analysis?.languages ?? []).map((l) => ({ ...l, language: l.name })),
+ frameworks: analysis?.frameworks ?? [],
+ entryPoints: analysis?.entryPoints ?? [],
+ subsystems,
+ concepts,
+ largestModules,
+ dependencies: {
+ declared: analysis?.declaredDependencies.length ?? 0,
+ imported: externalPackages.filter((p) => p.importers > 0).length,
+ internalImports,
+ externalPackages: externalPackages.slice(0, 200),
+ },
+ architectureSummary: summary,
+ unresolved: analysis?.unresolvedCount ?? 0,
+ validation: { errors: analysis?.validation.errors ?? 0, warnings: analysis?.validation.warnings ?? 0 },
+ summary: analysis?.summary ?? null,
+ };
+}
diff --git a/packages/graph-engine/src/explore.ts b/packages/graph-engine/src/explore.ts
new file mode 100644
index 0000000..7269ee1
--- /dev/null
+++ b/packages/graph-engine/src/explore.ts
@@ -0,0 +1,547 @@
+/**
+ * Question-driven repository exploration.
+ *
+ * Questions are answered by querying the knowledge graph: the engine picks an
+ * intent, locates the subject node, retrieves the relevant relationships and
+ * returns statements that each cite files and lines. Anything the graph does
+ * not establish is listed under `caveats` instead of being asserted.
+ */
+import {
+ EDGE_TYPE_LABELS,
+ type EdgeType,
+ type GraphEdge,
+ type GraphNode,
+ NODE_TYPE_LABELS,
+ isSymbolType,
+} from '@vectora/graph-schema';
+import type { EvidenceRef, ExplorationAnswer, ExplorationStatement } from '@vectora/shared';
+import { type GraphIndex, containerChain, cyclePath, findCycles, impactOf, traverse } from './algorithms';
+import type { RepositoryAnalysis } from './analysis';
+
+export type NodeFinder = (query: string, limit?: number) => GraphNode[];
+
+type Intent =
+ 'cycles' | 'startup' | 'callers' | 'dependents' | 'impact' | 'data-access' | 'trace' | 'explain';
+
+const INTENTS: [Intent, RegExp][] = [
+ ['cycles', /\b(circular|cycles?|cyclic)\b/i],
+ ['startup', /\b(start[- ]?up|boot(strap)?|entry points?|application start|launch sequence)\b/i],
+ [
+ 'callers',
+ /\b(what|who|which)\b.*\bcalls?\b|\bcallers? of\b|\bcalled by\b|\bwhere is .* (called|used)\b/i,
+ ],
+ [
+ 'impact',
+ /\b(affected|impact|break|breaks|ripple)\b|\bif i (change|modify|remove|delete|rename)\b|\bchang(e|ing)\b.*\b(affect|impact)/i,
+ ],
+ [
+ 'dependents',
+ /\bdepend(s|ents?)? on\b|\bdependents of\b|\bwhich .* (use|uses|import|imports)\b|\bwho uses\b/i,
+ ],
+ ['data-access', /\b(database|data|db|storage)\b.*\b(access|path|flow|queries|reads?|writes?)\b/i],
+ ['trace', /\b(trace|flow of|walk through|how does .* work|process|sequence|path from)\b/i],
+ ['explain', /\b(explain|what is|what does|describe|tell me about|overview of|summari[sz]e)\b/i],
+];
+
+const STOP = new Set(
+ (
+ 'a an the this that these those is are was were be been do does did what which who whom where when why how can could would should i me my we our you your it its of in on at to ' +
+ 'for from by with about into through and or if then than as so explain describe show tell trace identify list find give complete entire whole all any some ' +
+ 'calls call called caller callers depend depends dependents dependency dependencies on affected affect impact change changing changed modify remove delete rename ' +
+ 'break breaks module modules function functions class classes method methods service services file files process flow path access sequence overview work works ' +
+ 'happens happen use uses used using import imports there here please could'
+ ).split(' '),
+);
+
+const ev = (n: GraphNode | undefined, e?: GraphEdge): EvidenceRef[] => {
+ const out: EvidenceRef[] = [];
+ if (e?.location)
+ out.push({
+ path: e.location.path,
+ startLine: e.location.startLine,
+ endLine: e.location.endLine,
+ snippet: e.evidence,
+ nodeId: e.source,
+ });
+ else if (n?.location)
+ out.push({
+ path: n.location.path,
+ startLine: n.location.startLine,
+ endLine: n.location.endLine,
+ nodeId: n.id,
+ });
+ return out;
+};
+const where = (n: GraphNode | undefined): string =>
+ n?.location ? ` (${n.location.path}:${n.location.startLine})` : n?.filePath ? ` (${n.filePath})` : '';
+const label = (n: GraphNode): string => `${NODE_TYPE_LABELS[n.type].toLowerCase()} \`${n.name}\``;
+
+export function detectIntent(question: string): Intent {
+ for (const [intent, re] of INTENTS) if (re.test(question)) return intent;
+ return 'explain';
+}
+
+/** Extracts the subject of a question: quoted or code-formatted text first, otherwise the remaining keywords. */
+export function extractSubject(question: string): string {
+ const quoted = /[`"'“]([^`"'”]{2,80})[`"'”]/.exec(question);
+ if (quoted) return quoted[1]!.trim();
+ const words = question
+ .replace(/[?!.,;:()]/g, ' ')
+ .split(/\s+/)
+ .filter((w) => w && !STOP.has(w.toLowerCase()));
+ // Prefer tokens that look like identifiers or paths.
+ const ident = words.filter((w) => /[A-Z].*[a-z]|[a-z][A-Z]|[_/.]|\d/.test(w));
+ return (ident.length ? ident : words).join(' ').trim();
+}
+
+export function explore(
+ question: string,
+ index: GraphIndex,
+ analysis: RepositoryAnalysis | null,
+ find: NodeFinder,
+): ExplorationAnswer {
+ const intent = detectIntent(question);
+ const subject = extractSubject(question);
+ const answer: ExplorationAnswer = {
+ question,
+ intent,
+ title: '',
+ statements: [],
+ nodeIds: [],
+ edgeIds: [],
+ caveats: [],
+ };
+ const say = (text: string, evidence: EvidenceRef[], verified = true) =>
+ answer.statements.push({ text, verified, evidence } satisfies ExplorationStatement);
+ const mark = (...ids: string[]) => {
+ for (const id of ids)
+ if (!answer.nodeIds.includes(id) && answer.nodeIds.length < 300) answer.nodeIds.push(id);
+ };
+ const markEdge = (e: GraphEdge) => {
+ if (answer.edgeIds.length < 600) answer.edgeIds.push(e.id);
+ };
+ const node = (id: string) => index.nodes.get(id);
+
+ const locate = (preferred?: (n: GraphNode) => boolean): GraphNode | null => {
+ if (!subject) return null;
+ const candidates = find(subject, 12);
+ if (!candidates.length) {
+ // Retry with individual words, longest first.
+ for (const w of subject.split(/\s+/).sort((a, b) => b.length - a.length)) {
+ const c = find(w, 8);
+ if (c.length) return (preferred && c.find(preferred)) || c[0]!;
+ }
+ return null;
+ }
+ return (preferred && candidates.slice(0, 5).find(preferred)) || candidates[0]!;
+ };
+ const notFound = () => {
+ answer.title = subject ? `No match for “${subject}”` : 'No subject found';
+ answer.caveats.push(
+ subject
+ ? `Nothing in the indexed graph matches “${subject}”. Try an exact symbol name, a file path or a concept.`
+ : 'The question does not name a symbol, file or concept that could be looked up.',
+ );
+ return answer;
+ };
+
+ switch (intent) {
+ case 'cycles': {
+ const cycles = findCycles(index, ['IMPORTS'], (n) => n.type === 'File');
+ answer.title = 'Circular dependencies';
+ if (!cycles.length) {
+ say('No circular imports between files were found in the indexed graph.', []);
+ answer.caveats.push(
+ 'Only import relationships that were resolved to files in this repository are considered.',
+ );
+ return answer;
+ }
+ say(
+ `${cycles.length} group${cycles.length === 1 ? '' : 's'} of files import each other in a cycle.`,
+ [],
+ );
+ for (const comp of cycles.slice(0, 8)) {
+ const path = cyclePath(index, comp);
+ mark(...comp);
+ path.forEach(markEdge);
+ say(
+ `${comp.length} files: ${path.map((e) => node(e.source)?.filePath).join(' → ')} → ${node(path[0]?.source ?? comp[0]!)?.filePath}`,
+ path.slice(0, 6).flatMap((e) => ev(undefined, e)),
+ );
+ }
+ if (cycles.length > 8) answer.caveats.push(`${cycles.length - 8} further cycle groups are not listed.`);
+ return answer;
+ }
+
+ case 'startup': {
+ answer.title = 'Application startup';
+ const entries = analysis?.entryPoints ?? [];
+ if (!entries.length) {
+ answer.caveats.push('No entry point could be identified from manifests or file conventions.');
+ return answer;
+ }
+ for (const e of entries.slice(0, 6)) {
+ const n = node(e.nodeId);
+ mark(e.nodeId);
+ say(`\`${e.path}\` is an entry point: ${e.reason}.`, ev(n));
+ const imports = (index.out.get(e.nodeId) ?? []).filter(
+ (x) => x.type === 'IMPORTS' && x.target.startsWith('file:'),
+ );
+ if (imports.length) {
+ imports.slice(0, 12).forEach((x) => (markEdge(x), mark(x.target)));
+ say(
+ `It imports ${imports.length} internal file${imports.length === 1 ? '' : 's'}: ${imports
+ .slice(0, 8)
+ .map((x) => `\`${node(x.target)?.filePath}\``)
+ .join(', ')}${imports.length > 8 ? ', ...' : ''}.`,
+ imports.slice(0, 4).flatMap((x) => ev(undefined, x)),
+ );
+ }
+ }
+ for (const f of (analysis?.flows ?? []).filter((f) => f.kind === 'startup').slice(0, 4)) {
+ mark(...f.steps);
+ say(
+ `Call chain from startup: ${f.steps.map((s) => `\`${node(s)?.name ?? s}\``).join(' → ')}.`,
+ f.steps.slice(0, 5).flatMap((s) => ev(node(s))),
+ false,
+ );
+ }
+ answer.caveats.push(
+ 'Startup order is inferred from imports and resolved calls; runtime configuration can change it.',
+ );
+ return answer;
+ }
+
+ case 'callers': {
+ const target = locate(
+ (n) => n.type === 'Function' || n.type === 'Method' || n.type === 'Component' || n.type === 'Class',
+ );
+ if (!target) return notFound();
+ answer.title = `Callers of ${target.name}`;
+ mark(target.id);
+ const ids = [
+ target.id,
+ ...(index.out.get(target.id) ?? []).filter((e) => e.type === 'CONTAINS').map((e) => e.target),
+ ];
+ const calls = ids.flatMap((id) =>
+ (index.in.get(id) ?? []).filter((e) =>
+ ['CALLS', 'INSTANTIATES', 'ROUTES_TO', 'USES'].includes(e.type),
+ ),
+ );
+ say(
+ `${label(target)} is defined at ${target.location?.path}:${target.location?.startLine}.`,
+ ev(target),
+ );
+ if (!calls.length) {
+ say('No resolved call, instantiation or route points to it.', []);
+ answer.caveats.push(
+ 'Calls made through dynamic dispatch, reflection or unresolved references are not visible in the graph.',
+ );
+ return answer;
+ }
+ for (const e of calls.slice(0, 40)) {
+ const src = node(e.source);
+ mark(e.source);
+ markEdge(e);
+ say(
+ `${src ? label(src) : e.source} ${EDGE_TYPE_LABELS[e.type]} \`${node(e.target)?.name}\`.`,
+ ev(src, e),
+ e.verified,
+ );
+ }
+ if (calls.length > 40) answer.caveats.push(`${calls.length - 40} further references are not listed.`);
+ return answer;
+ }
+
+ case 'dependents':
+ case 'impact': {
+ const target = locate();
+ if (!target) return notFound();
+ const r = impactOf(index, target.id, intent === 'impact' ? 5 : 2, 400);
+ answer.title =
+ intent === 'impact' ? `Impact of changing ${target.name}` : `What depends on ${target.name}`;
+ mark(target.id);
+ say(`${label(target)}${where(target)} is the subject.`, ev(target));
+ const direct = [...r.distances.entries()].filter(([, d]) => d === 1).map(([id]) => id);
+ const transitive = [...r.distances.entries()].filter(([, d]) => d > 1);
+ if (!direct.length) {
+ say('Nothing in the indexed graph depends on it.', []);
+ answer.caveats.push('Unresolved or dynamic references cannot be ruled out.');
+ return answer;
+ }
+ r.edges.forEach(markEdge);
+ const byFile = new Map();
+ for (const id of direct) {
+ const n = node(id);
+ if (!n) continue;
+ mark(id);
+ const key = n.filePath ?? n.id;
+ (byFile.get(key) ?? byFile.set(key, []).get(key)!).push(n);
+ }
+ say(
+ `${direct.length} node${direct.length === 1 ? '' : 's'} in ${byFile.size} file${byFile.size === 1 ? '' : 's'} depend on it directly.`,
+ [],
+ );
+ for (const [file, list] of [...byFile.entries()].slice(0, 25)) {
+ const e = r.edges.find((x) => list.some((n) => n.id === x.source));
+ say(
+ `\`${file}\`: ${list
+ .slice(0, 6)
+ .map((n) => `\`${n.name}\``)
+ .join(', ')}${list.length > 6 ? ', ...' : ''}${e ? ` — ${EDGE_TYPE_LABELS[e.type]}` : ''}.`,
+ e ? ev(undefined, e) : ev(list[0]),
+ e?.verified ?? true,
+ );
+ }
+ if (intent === 'impact' && transitive.length) {
+ transitive.slice(0, 150).forEach(([id]) => mark(id));
+ const files = new Set(transitive.map(([id]) => node(id)?.filePath).filter(Boolean));
+ say(
+ `${transitive.length} further node${transitive.length === 1 ? '' : 's'} across ${files.size} file${files.size === 1 ? '' : 's'} are reachable through those dependants (up to 5 steps).`,
+ [],
+ false,
+ );
+ const subs = new Set(
+ [...r.distances.keys()]
+ .map((id) => node(id)?.metadata.subsystem as string | undefined)
+ .filter(Boolean),
+ );
+ if (subs.size > 1)
+ say(
+ `The change crosses ${subs.size} subsystems: ${[...subs].map((s) => node(s!)?.name).join(', ')}.`,
+ [],
+ false,
+ );
+ }
+ if (r.truncated) answer.caveats.push('The result was truncated at 400 nodes.');
+ answer.caveats.push(
+ 'Impact is computed from resolved relationships only; unresolved references and runtime wiring are not included.',
+ );
+ return answer;
+ }
+
+ case 'data-access': {
+ answer.title = 'Database access paths';
+ const dataEdges = [...index.edges.values()].filter(
+ (e) => e.type === 'READS_FROM' || e.type === 'WRITES_TO',
+ );
+ const models = [...index.nodes.values()].filter((n) => n.type === 'DatabaseModel');
+ if (!models.length && !dataEdges.length) {
+ answer.caveats.push(
+ 'No database models or data-access relationships were detected in this repository.',
+ );
+ return answer;
+ }
+ for (const m of models.slice(0, 15)) {
+ mark(m.id);
+ const inc = (index.in.get(m.id) ?? []).filter(
+ (e) => e.type === 'READS_FROM' || e.type === 'WRITES_TO',
+ );
+ say(
+ `Model \`${m.name}\`${where(m)} — ${inc.filter((e) => e.type === 'READS_FROM').length} readers, ${inc.filter((e) => e.type === 'WRITES_TO').length} writers.`,
+ ev(m),
+ );
+ for (const e of inc.slice(0, 6)) {
+ mark(e.source);
+ markEdge(e);
+ say(
+ `\`${node(e.source)?.name}\` ${EDGE_TYPE_LABELS[e.type]} \`${m.name}\`${e.metadata?.operation ? ` via \`${String(e.metadata.operation)}\`` : ''}.`,
+ ev(node(e.source), e),
+ e.verified,
+ );
+ }
+ }
+ for (const f of (analysis?.flows ?? []).filter((f) => f.reachesData).slice(0, 8)) {
+ mark(...f.steps);
+ say(
+ `${f.name}: ${f.steps.map((s) => `\`${node(s)?.name ?? s}\``).join(' → ')}.`,
+ f.steps.slice(0, 6).flatMap((s) => ev(node(s))),
+ false,
+ );
+ }
+ answer.caveats.push(
+ 'Read and write relationships are detected from call patterns on known models and are marked as derived.',
+ );
+ return answer;
+ }
+
+ case 'trace': {
+ const target = locate((n) => n.type === 'ApiEndpoint' || n.type === 'Function' || n.type === 'Method');
+ if (!target) return notFound();
+ answer.title = `Trace: ${target.name}`;
+ let starts: GraphNode[] = [target];
+ if (
+ target.type === 'Concept' ||
+ target.type === 'Subsystem' ||
+ target.type === 'File' ||
+ target.type === 'Module' ||
+ target.type === 'Directory'
+ ) {
+ const files = new Set(
+ target.type === 'File'
+ ? [target.id]
+ : (index.in.get(target.id) ?? []).filter((e) => e.type === 'BELONGS_TO').map((e) => e.source),
+ );
+ const inFiles = [...index.nodes.values()].filter(
+ (n) => n.filePath && files.has(`file:${n.filePath}`),
+ );
+ const endpoints = inFiles.filter((n) => n.type === 'ApiEndpoint');
+ const tokens = subject.toLowerCase().split(/\s+/);
+ const named = inFiles.filter(
+ (n) =>
+ (n.type === 'Function' || n.type === 'Method') &&
+ tokens.some((t) => n.name.toLowerCase().includes(t)),
+ );
+ starts = (endpoints.length ? endpoints : named).slice(0, 4);
+ say(
+ `${label(target)} groups ${files.size} file${files.size === 1 ? '' : 's'}; tracing from ${starts.length ? starts.map((s) => `\`${s.name}\``).join(', ') : 'no callable entry'}.`,
+ [],
+ target.verified,
+ );
+ if (!starts.length) {
+ answer.caveats.push('No endpoint or function inside this group could be used as a starting point.');
+ return answer;
+ }
+ }
+ for (const s of starts) {
+ mark(s.id);
+ say(`Starts at ${label(s)}${where(s)}.`, ev(s));
+ const r = traverse(index, s.id, 'out', {
+ types: ['ROUTES_TO', 'CALLS', 'INSTANTIATES', 'READS_FROM', 'WRITES_TO'],
+ maxDepth: 6,
+ maxNodes: 80,
+ });
+ if (r.distances.size === 1) {
+ say('No outgoing calls were resolved from here.', []);
+ continue;
+ }
+ const seen = new Set();
+ for (const e of r.edges) {
+ if (seen.has(e.id)) continue;
+ seen.add(e.id);
+ if (seen.size > 40) break;
+ markEdge(e);
+ mark(e.source, e.target);
+ const depth = r.distances.get(e.source) ?? 0;
+ say(
+ `${'· '.repeat(depth)}\`${node(e.source)?.name}\` ${EDGE_TYPE_LABELS[e.type]} \`${node(e.target)?.name}\`${where(node(e.target))}.`,
+ ev(node(e.source), e),
+ e.verified,
+ );
+ }
+ if (r.truncated || r.edges.length > 40)
+ answer.caveats.push('The trace was shortened; expand nodes in the graph to follow it further.');
+ }
+ answer.caveats.push(
+ 'Only statically resolved calls are followed. Callbacks, events and framework wiring may add steps that are not shown.',
+ );
+ return answer;
+ }
+
+ case 'explain':
+ default: {
+ const target = locate((n) => n.type === 'Concept' || n.type === 'Subsystem');
+ if (!target) return notFound();
+ answer.title = target.name;
+ mark(target.id);
+ const outE = index.out.get(target.id) ?? [];
+ const inE = index.in.get(target.id) ?? [];
+ if (target.type === 'Concept' || target.type === 'Subsystem') {
+ const members = inE
+ .filter((e) => e.type === 'BELONGS_TO')
+ .sort((a, b) => b.confidence - a.confidence);
+ say(
+ `${target.name} is a ${target.type === 'Concept' ? 'concept group derived from identifier and path evidence' : 'subsystem derived from the repository layout'} with ${members.length} file${members.length === 1 ? '' : 's'}.`,
+ [],
+ target.verified,
+ );
+ for (const m of members.slice(0, 12)) {
+ const f = node(m.source);
+ mark(m.source);
+ markEdge(m);
+ const syms = (index.out.get(m.source) ?? [])
+ .filter((e) => e.type === 'CONTAINS')
+ .map((e) => node(e.target))
+ .filter((n): n is GraphNode => !!n && isSymbolType(n.type));
+ say(
+ `\`${f?.filePath}\` — ${m.evidence}${
+ syms.length
+ ? `. Defines ${syms
+ .slice(0, 6)
+ .map((s) => `\`${s.name}\``)
+ .join(', ')}${syms.length > 6 ? ', ...' : ''}`
+ : ''
+ }.`,
+ ev(f),
+ m.verified,
+ );
+ }
+ const deps = [...outE, ...inE].filter((e) => e.type === 'DEPENDS_ON' || e.type === 'RELATED_TO');
+ for (const d of deps.slice(0, 8)) {
+ markEdge(d);
+ mark(d.source, d.target);
+ say(
+ `${node(d.source)?.name} ${EDGE_TYPE_LABELS[d.type]} ${node(d.target)?.name}: ${d.evidence}`,
+ ev(undefined, d),
+ d.verified,
+ );
+ }
+ if (members.length > 12)
+ answer.caveats.push(`${members.length - 12} further member files are not listed.`);
+ if (target.type === 'Concept')
+ answer.caveats.push(
+ 'Concept membership is an interpretation based on names and paths, not a verified fact.',
+ );
+ return answer;
+ }
+ const chain = containerChain(index, target.id);
+ say(
+ `${label(target)} is defined at ${target.location ? `${target.location.path}:${target.location.startLine}–${target.location.endLine}` : (target.filePath ?? 'an unknown location')}${
+ chain.length
+ ? `, inside ${chain
+ .slice(-2)
+ .map((c) => c.name)
+ .join(' / ')}`
+ : ''
+ }.`,
+ ev(target),
+ );
+ if (target.description) say(`Documented as: ${target.description}`, ev(target));
+ if (target.metadata.signature) say(`Signature: \`${String(target.metadata.signature)}\``, ev(target));
+ const group = (edges: GraphEdge[], dir: 'out' | 'in') => {
+ const byType = new Map();
+ for (const e of edges) {
+ if (e.type === 'CONTAINS' && dir === 'in') continue;
+ if (e.type === 'DEFINED_IN' || e.type === 'BELONGS_TO') continue;
+ (byType.get(e.type) ?? byType.set(e.type, []).get(e.type)!).push(e);
+ }
+ for (const [type, list] of byType) {
+ list.slice(0, 15).forEach((e) => (markEdge(e), mark(dir === 'out' ? e.target : e.source)));
+ const names = list
+ .slice(0, 8)
+ .map((e) => `\`${node(dir === 'out' ? e.target : e.source)?.name}\``)
+ .join(', ');
+ say(
+ dir === 'out'
+ ? `It ${EDGE_TYPE_LABELS[type]} ${names}${list.length > 8 ? ` and ${list.length - 8} more` : ''}.`
+ : `${names}${list.length > 8 ? ` and ${list.length - 8} more` : ''} ${list.length === 1 ? EDGE_TYPE_LABELS[type] : EDGE_TYPE_LABELS[type].replace(/s\b/, '')} it.`,
+ list.slice(0, 4).flatMap((e) => ev(node(e.source), e)),
+ list.every((e) => e.verified),
+ );
+ }
+ };
+ group(outE, 'out');
+ group(inE, 'in');
+ const concepts =
+ ((target.filePath ? node(`file:${target.filePath}`)?.metadata.concepts : null) as string[] | null) ??
+ [];
+ if (concepts.length)
+ say(`Its file is associated with: ${concepts.map((c) => node(c)?.name).join(', ')}.`, [], false);
+ if (outE.length + inE.length <= 1)
+ answer.caveats.push(
+ 'Few relationships were resolved for this node; it may be used dynamically or not at all.',
+ );
+ return answer;
+ }
+ }
+}
diff --git a/packages/graph-engine/src/exports.ts b/packages/graph-engine/src/exports.ts
new file mode 100644
index 0000000..dcb2e22
--- /dev/null
+++ b/packages/graph-engine/src/exports.ts
@@ -0,0 +1,336 @@
+/**
+ * Graph exports: JSON, GraphML, Markdown analysis and architecture
+ * documentation. Node ids and relationships are always preserved. Source
+ * text (evidence lines, signatures, doc comments) is only included when the
+ * caller explicitly asks for it.
+ */
+import {
+ EDGE_TYPE_LABELS,
+ type GraphEdge,
+ type GraphNode,
+ type KnowledgeGraph,
+ NODE_TYPE_LABELS,
+ isSymbolType,
+} from '@vectora/graph-schema';
+import { APP_VERSION, type RepositoryOverview, type RepositoryRecord } from '@vectora/shared';
+import { buildIndex, cyclePath, findCycles } from './algorithms';
+import type { RepositoryAnalysis } from './analysis';
+
+export interface ExportOptions {
+ includeSource?: boolean;
+}
+
+const SOURCE_METADATA_KEYS = ['signature', 'attributes', 'hash'];
+
+/** Removes source text from a graph so it can be shared without repository contents. */
+export function stripSource(graph: Pick): {
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+} {
+ const nodes = graph.nodes.map((n) => {
+ const metadata = { ...n.metadata };
+ for (const k of SOURCE_METADATA_KEYS) delete metadata[k];
+ const out: GraphNode = { ...n, metadata };
+ // Descriptions of symbols are doc comments, i.e. repository content.
+ if (isSymbolType(n.type) || n.type === 'Repository') delete out.description;
+ return out;
+ });
+ const edges = graph.edges.map((e) => ({
+ ...e,
+ evidence: e.location ? `${e.location.path}:${e.location.startLine}` : e.extractionMethod,
+ }));
+ return { nodes, edges };
+}
+
+export function exportJson(
+ repository: RepositoryRecord,
+ graph: KnowledgeGraph,
+ opts: ExportOptions = {},
+): string {
+ const { nodes, edges } = opts.includeSource ? graph : stripSource(graph);
+ return JSON.stringify(
+ {
+ format: 'vectora-graph',
+ schemaVersion: graph.schemaVersion,
+ exportedAt: new Date().toISOString(),
+ generator: `VECTORA ${APP_VERSION}`,
+ includesSource: !!opts.includeSource,
+ repository: {
+ id: repository.id,
+ name: repository.name,
+ sourceKind: repository.sourceKind,
+ branch: repository.branch,
+ lastIndexedAt: repository.lastIndexedAt,
+ },
+ stats: graph.stats,
+ nodes,
+ edges,
+ },
+ null,
+ 2,
+ );
+}
+
+const xml = (s: unknown): string =>
+ String(s ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ // eslint-disable-next-line no-control-regex
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
+
+export function exportGraphML(
+ repository: RepositoryRecord,
+ graph: KnowledgeGraph,
+ opts: ExportOptions = {},
+): string {
+ const { nodes, edges } = opts.includeSource ? graph : stripSource(graph);
+ const nodeKeys: [string, string, (n: GraphNode) => unknown][] = [
+ ['name', 'string', (n) => n.name],
+ ['type', 'string', (n) => n.type],
+ ['filePath', 'string', (n) => n.filePath ?? ''],
+ ['startLine', 'int', (n) => n.location?.startLine ?? ''],
+ ['endLine', 'int', (n) => n.location?.endLine ?? ''],
+ ['language', 'string', (n) => n.language ?? ''],
+ ['extractionMethod', 'string', (n) => n.extractionMethod],
+ ['confidence', 'double', (n) => n.confidence],
+ ['verified', 'boolean', (n) => n.verified],
+ ];
+ const edgeKeys: [string, string, (e: GraphEdge) => unknown][] = [
+ ['relationship', 'string', (e) => e.type],
+ ['evidence', 'string', (e) => e.evidence],
+ ['evidencePath', 'string', (e) => e.location?.path ?? ''],
+ ['evidenceLine', 'int', (e) => e.location?.startLine ?? ''],
+ ['edgeExtractionMethod', 'string', (e) => e.extractionMethod],
+ ['edgeConfidence', 'double', (e) => e.confidence],
+ ['edgeVerified', 'boolean', (e) => e.verified],
+ ];
+ const out: string[] = [
+ '',
+ '',
+ ` `,
+ ];
+ for (const [k, t] of nodeKeys) out.push(` `);
+ for (const [k, t] of edgeKeys) out.push(` `);
+ out.push(` `);
+ for (const n of nodes) {
+ out.push(` `);
+ for (const [k, , get] of nodeKeys) {
+ const v = get(n);
+ if (v !== '' && v !== null && v !== undefined) out.push(` ${xml(v)} `);
+ }
+ out.push(' ');
+ }
+ for (const e of edges) {
+ out.push(` `);
+ for (const [k, , get] of edgeKeys) {
+ const v = get(e);
+ if (v !== '' && v !== null && v !== undefined) out.push(` ${xml(v)} `);
+ }
+ out.push(' ');
+ }
+ out.push(' ', ' ', '');
+ return out.join('\n');
+}
+
+const mdEscape = (s: string): string => s.replace(/\|/g, '\\|').replace(/\n/g, ' ');
+
+export function exportMarkdownAnalysis(
+ overview: RepositoryOverview,
+ graph: KnowledgeGraph,
+ analysis: RepositoryAnalysis | null,
+): string {
+ const r = overview.repository;
+ const L: string[] = [];
+ L.push(`# ${r.name} — repository analysis`, '');
+ L.push(`Generated by VECTORA ${APP_VERSION} on ${new Date().toISOString().slice(0, 10)}.`, '');
+ if (overview.description)
+ L.push(`> ${overview.description}`, '', `Source: \`${overview.descriptionSource}\``, '');
+ L.push('## Index', '');
+ L.push(`| | |`, `|---|---|`);
+ L.push(`| Source | ${r.sourceKind} |`);
+ if (r.branch) L.push(`| Branch | ${r.branch} |`);
+ L.push(`| Last indexed | ${r.lastIndexedAt ?? 'never'} |`);
+ L.push(
+ `| Files | ${graph.stats.fileCount} |`,
+ `| Symbols | ${graph.stats.symbolCount} |`,
+ `| Relationships | ${graph.stats.edgeCount} |`,
+ );
+ L.push(
+ `| Verified relationships | ${graph.stats.verifiedEdges} |`,
+ `| Derived relationships | ${graph.stats.unverifiedEdges} |`,
+ `| Unresolved references | ${overview.unresolved} |`,
+ '',
+ );
+
+ L.push(
+ '## Languages',
+ '',
+ '| Language | Files | Analysed | Symbols | Technique |',
+ '|---|---:|---:|---:|---|',
+ );
+ for (const l of overview.languages)
+ L.push(
+ `| ${l.language} | ${l.files} | ${l.analyzedFiles} | ${l.symbols} | ${l.adapter ?? 'not analysed at symbol level'} |`,
+ );
+ L.push('');
+ if (overview.frameworks.length) {
+ L.push('## Frameworks', '');
+ for (const f of overview.frameworks) L.push(`- **${f.name}** — ${f.evidence}`);
+ L.push('');
+ }
+ L.push('## Architecture summary', '');
+ for (const s of overview.architectureSummary)
+ L.push(`- ${s.statement}${s.verified ? '' : ' _(derived)_'} \n Evidence: ${s.evidence}`);
+ L.push('');
+ if (overview.entryPoints.length) {
+ L.push('## Entry points', '');
+ for (const e of overview.entryPoints) L.push(`- \`${e.path}\` — ${e.reason}`);
+ L.push('');
+ }
+ if (overview.subsystems.length) {
+ L.push(
+ '## Subsystems',
+ '',
+ '| Subsystem | Files | Symbols | Outbound | Inbound |',
+ '|---|---:|---:|---:|---:|',
+ );
+ for (const s of overview.subsystems)
+ L.push(`| ${mdEscape(s.name)} | ${s.files} | ${s.symbols} | ${s.outbound} | ${s.inbound} |`);
+ L.push('');
+ }
+ if (overview.concepts.length) {
+ L.push(
+ '## Concepts',
+ '',
+ 'Concept groups are derived from identifier and path evidence; they are interpretations, not verified facts.',
+ '',
+ );
+ for (const c of overview.concepts) L.push(`- **${c.name}** — ${c.members} files`);
+ L.push('');
+ }
+ L.push('## Largest modules', '', '| File | Symbols | Lines |', '|---|---:|---:|');
+ for (const m of overview.largestModules) L.push(`| \`${m.path}\` | ${m.symbols} | ${m.lines} |`);
+ L.push('');
+ L.push('## Dependencies', '');
+ L.push(
+ `${overview.dependencies.declared} declared in manifests, ${overview.dependencies.imported} imported by source files, ${overview.dependencies.internalImports} internal imports.`,
+ '',
+ );
+ const top = overview.dependencies.externalPackages.filter((p) => p.importers > 0).slice(0, 30);
+ if (top.length) {
+ L.push('| Package | Importing files | Declared | Version |', '|---|---:|---|---|');
+ for (const p of top)
+ L.push(`| ${mdEscape(p.name)} | ${p.importers} | ${p.declared ? 'yes' : 'no'} | ${p.version ?? ''} |`);
+ L.push('');
+ }
+ if (analysis && analysis.unresolvedCount) {
+ L.push('## Unresolved references', '');
+ for (const [reason, count] of Object.entries(analysis.unresolvedByReason))
+ L.push(`- ${count} × ${reason}`);
+ L.push('');
+ }
+ return L.join('\n');
+}
+
+export function exportArchitectureDoc(
+ overview: RepositoryOverview,
+ graph: KnowledgeGraph,
+ analysis: RepositoryAnalysis | null,
+): string {
+ const idx = buildIndex(graph);
+ const name = (id: string) => idx.nodes.get(id)?.name ?? id;
+ const L: string[] = [];
+ L.push(`# ${overview.repository.name} — architecture`, '');
+ L.push(
+ `Generated by VECTORA ${APP_VERSION} from the knowledge graph (${graph.stats.nodeCount} nodes, ${graph.stats.edgeCount} relationships).`,
+ '',
+ );
+ L.push('Statements marked _derived_ are interpretations built on top of verified structure.', '');
+ L.push('## Subsystems', '');
+ for (const s of overview.subsystems) {
+ const node = idx.nodes.get(s.id);
+ L.push(`### ${s.name}`, '');
+ L.push(`${node?.description ?? ''} ${s.files} files, ${s.symbols} symbols.`.trim(), '');
+ const deps = (idx.out.get(s.id) ?? []).filter((e) => e.type === 'DEPENDS_ON');
+ if (deps.length) {
+ L.push('Depends on:', '');
+ for (const d of deps.sort((a, b) => Number(b.metadata?.count ?? 0) - Number(a.metadata?.count ?? 0)))
+ L.push(
+ `- **${name(d.target)}** — ${d.metadata?.count} relationships (${d.location ? `\`${d.location.path}:${d.location.startLine}\`` : 'aggregated'})`,
+ );
+ L.push('');
+ }
+ const users = (idx.in.get(s.id) ?? []).filter((e) => e.type === 'DEPENDS_ON');
+ if (users.length) L.push(`Used by: ${users.map((u) => name(u.source)).join(', ')}.`, '');
+ }
+ const deps = graph.edges.filter((e) => e.type === 'DEPENDS_ON' && e.source.startsWith('sub:'));
+ if (deps.length) {
+ L.push('## Subsystem dependency diagram', '', '```mermaid', 'graph LR');
+ const mid = (id: string) => `n${[...idx.nodes.keys()].indexOf(id)}`;
+ const ids = new Set();
+ for (const d of deps) {
+ ids.add(d.source);
+ ids.add(d.target);
+ }
+ for (const id of ids) L.push(` ${mid(id)}["${name(id).replace(/"/g, "'")}"]`);
+ for (const d of deps) L.push(` ${mid(d.source)} -->|${d.metadata?.count ?? ''}| ${mid(d.target)}`);
+ L.push('```', '');
+ }
+ if (overview.entryPoints.length) {
+ L.push('## Entry points', '');
+ for (const e of overview.entryPoints) L.push(`- \`${e.path}\` — ${e.reason}`);
+ L.push('');
+ }
+ const endpoints = graph.nodes.filter((n) => n.type === 'ApiEndpoint');
+ if (endpoints.length) {
+ L.push('## API endpoints', '', '| Endpoint | Defined at | Handler |', '|---|---|---|');
+ for (const e of endpoints.slice(0, 200)) {
+ const h = (idx.out.get(e.id) ?? []).find((x) => x.type === 'ROUTES_TO');
+ L.push(
+ `| \`${mdEscape(e.name)}\` | \`${e.filePath}:${e.location?.startLine}\` | ${h ? `\`${mdEscape(name(h.target))}\`` : 'inline'} |`,
+ );
+ }
+ L.push('');
+ }
+ const models = graph.nodes.filter((n) => n.type === 'DatabaseModel');
+ if (models.length) {
+ L.push('## Database models', '', '| Model | Defined at | Readers | Writers |', '|---|---|---:|---:|');
+ for (const m of models) {
+ const inc = idx.in.get(m.id) ?? [];
+ L.push(
+ `| \`${mdEscape(m.name)}\` | \`${m.filePath}:${m.location?.startLine}\` | ${inc.filter((e) => e.type === 'READS_FROM').length} | ${inc.filter((e) => e.type === 'WRITES_TO').length} |`,
+ );
+ }
+ L.push('');
+ }
+ if (analysis?.flows.length) {
+ L.push('## Execution flows', '', '_Derived by following route and call relationships._', '');
+ for (const f of analysis.flows.slice(0, 40))
+ L.push(
+ `- **${f.name}**${f.reachesData ? ' (reaches data access)' : ''}: ${f.steps.map((s) => `\`${name(s)}\``).join(' → ')}`,
+ );
+ L.push('');
+ }
+ const cycles = findCycles(idx, ['IMPORTS'], (n) => n.type === 'File');
+ L.push('## Circular dependencies', '');
+ if (!cycles.length) L.push('No circular imports between files were found.', '');
+ for (const c of cycles.slice(0, 20)) {
+ const path = cyclePath(idx, c);
+ L.push(
+ `- ${c.length} files: ${path.map((e) => `\`${idx.nodes.get(e.source)?.filePath}\``).join(' → ')} → \`${idx.nodes.get(path[0]?.source ?? c[0]!)?.filePath}\``,
+ );
+ }
+ L.push('');
+ L.push('## Relationship legend', '');
+ for (const [k, v] of Object.entries(EDGE_TYPE_LABELS))
+ L.push(`- \`${k}\` — ${v} (${graph.stats.edgesByType[k as keyof typeof EDGE_TYPE_LABELS] ?? 0})`);
+ L.push('', '## Node types', '');
+ for (const [k, v] of Object.entries(NODE_TYPE_LABELS)) {
+ const c = graph.stats.nodesByType[k as keyof typeof NODE_TYPE_LABELS] ?? 0;
+ if (c) L.push(`- ${v}: ${c}`);
+ }
+ L.push('');
+ return L.join('\n');
+}
diff --git a/packages/graph-engine/src/index.ts b/packages/graph-engine/src/index.ts
new file mode 100644
index 0000000..6d420c6
--- /dev/null
+++ b/packages/graph-engine/src/index.ts
@@ -0,0 +1,5 @@
+export * from './algorithms';
+export * from './analysis';
+export * from './exports';
+export * from './explore';
+export * from './store';
diff --git a/packages/graph-engine/src/store.ts b/packages/graph-engine/src/store.ts
new file mode 100644
index 0000000..81f9c84
--- /dev/null
+++ b/packages/graph-engine/src/store.ts
@@ -0,0 +1,455 @@
+/**
+ * SQLite persistence for repositories, knowledge graphs, cached per-file
+ * analysis, sessions and settings. Uses the SQLite engine built into Node.js
+ * (`node:sqlite`), so no native module has to be compiled or downloaded.
+ */
+import fs from 'node:fs';
+import { createRequire } from 'node:module';
+import path from 'node:path';
+import type { DatabaseSync, StatementSync } from 'node:sqlite';
+import {
+ type GraphEdge,
+ type GraphNode,
+ type KnowledgeGraph,
+ type NodeType,
+ SCHEMA_VERSION,
+ computeStats,
+} from '@vectora/graph-schema';
+import {
+ type RepositoryRecord,
+ type SessionState,
+ type Settings,
+ SettingsSchema,
+ defaultSettings,
+} from '@vectora/shared';
+
+const DB_VERSION = 2;
+
+export interface StoredRepository extends RepositoryRecord {
+ /** Absolute path of the indexed source tree on this machine. */
+ rootPath: string;
+ /** True when VECTORA owns the source copy (GitHub / ZIP imports) and may delete it. */
+ managedSource: boolean;
+}
+
+export interface CachedFacts {
+ path: string;
+ hash: string;
+ facts: T;
+}
+
+function openDatabase(file: string): DatabaseSync {
+ // `node:sqlite` prints an ExperimentalWarning on first load; it is expected here.
+ const original = process.emitWarning;
+ process.emitWarning = ((warning: string | Error, ...rest: unknown[]) => {
+ const text = typeof warning === 'string' ? warning : warning.message;
+ if (/SQLite is an experimental feature/.test(text)) return;
+ return (original as (...a: unknown[]) => void).call(process, warning, ...rest);
+ }) as typeof process.emitWarning;
+ try {
+ const require = createRequire(import.meta.url);
+ const sqlite = require('node:sqlite') as typeof import('node:sqlite');
+ return new sqlite.DatabaseSync(file);
+ } finally {
+ process.emitWarning = original;
+ }
+}
+
+type Row = Record;
+
+export class Store {
+ private db: DatabaseSync;
+ private statements = new Map();
+
+ constructor(public readonly file: string) {
+ if (file !== ':memory:') fs.mkdirSync(path.dirname(file), { recursive: true });
+ this.db = openDatabase(file);
+ this.db.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA foreign_keys = ON;');
+ this.migrate();
+ }
+
+ private migrate(): void {
+ // Version 1 stored graph rows in WITHOUT ROWID tables, which is slow for large JSON rows.
+ // Those tables are rebuilt; affected repositories are marked for re-indexing.
+ const old = this.db
+ .prepare(
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('nodes', 'edges', 'file_facts') AND sql LIKE '%WITHOUT ROWID%'",
+ )
+ .all() as Row[];
+ if (old.length) {
+ for (const t of old) this.db.exec(`DROP TABLE ${t.name as string}`);
+ this.db.exec(
+ "UPDATE repositories SET status = 'failed', error = 'The graph store was upgraded. Re-index this repository.'",
+ );
+ }
+ this.db.exec(`
+ CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
+ CREATE TABLE IF NOT EXISTS repositories (
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, source_kind TEXT NOT NULL, origin TEXT NOT NULL,
+ branch TEXT, subdirectory TEXT, root_path TEXT NOT NULL, managed_source INTEGER NOT NULL DEFAULT 0,
+ status TEXT NOT NULL, created_at TEXT NOT NULL, last_indexed_at TEXT,
+ file_count INTEGER NOT NULL DEFAULT 0, symbol_count INTEGER NOT NULL DEFAULT 0, relationship_count INTEGER NOT NULL DEFAULT 0,
+ error TEXT, analysis TEXT
+ );
+ CREATE TABLE IF NOT EXISTS nodes (
+ repo_id TEXT NOT NULL, id TEXT NOT NULL, type TEXT NOT NULL, name TEXT NOT NULL, file_path TEXT, data TEXT NOT NULL,
+ UNIQUE (repo_id, id)
+ );
+ CREATE INDEX IF NOT EXISTS nodes_type ON nodes (repo_id, type);
+ CREATE INDEX IF NOT EXISTS nodes_path ON nodes (repo_id, file_path);
+ CREATE TABLE IF NOT EXISTS edges (
+ repo_id TEXT NOT NULL, id TEXT NOT NULL, source TEXT NOT NULL, target TEXT NOT NULL, type TEXT NOT NULL, data TEXT NOT NULL,
+ UNIQUE (repo_id, id)
+ );
+ CREATE INDEX IF NOT EXISTS edges_source ON edges (repo_id, source);
+ CREATE INDEX IF NOT EXISTS edges_target ON edges (repo_id, target);
+ CREATE TABLE IF NOT EXISTS file_facts (
+ repo_id TEXT NOT NULL, path TEXT NOT NULL, hash TEXT NOT NULL, facts TEXT NOT NULL,
+ UNIQUE (repo_id, path)
+ );
+ CREATE TABLE IF NOT EXISTS sessions (repo_id TEXT PRIMARY KEY, data TEXT NOT NULL);
+ CREATE TABLE IF NOT EXISTS settings (id INTEGER PRIMARY KEY CHECK (id = 1), data TEXT NOT NULL);
+ CREATE TABLE IF NOT EXISTS credentials (name TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL);
+ `);
+ const v = this.db.prepare('SELECT value FROM meta WHERE key = ?').get('db_version') as Row | undefined;
+ if (!v)
+ this.db.prepare('INSERT INTO meta (key, value) VALUES (?, ?)').run('db_version', String(DB_VERSION));
+ else if (Number(v.value) < DB_VERSION)
+ this.db.prepare('UPDATE meta SET value = ? WHERE key = ?').run(String(DB_VERSION), 'db_version');
+ }
+
+ private stmt(sql: string): StatementSync {
+ let s = this.statements.get(sql);
+ if (!s) {
+ s = this.db.prepare(sql);
+ this.statements.set(sql, s);
+ }
+ return s;
+ }
+
+ private transaction(fn: () => T): T {
+ this.db.exec('BEGIN');
+ try {
+ const r = fn();
+ this.db.exec('COMMIT');
+ return r;
+ } catch (err) {
+ this.db.exec('ROLLBACK');
+ throw err;
+ }
+ }
+
+ private bulkInsert(
+ table: string,
+ columns: string[],
+ rows: readonly T[],
+ values: (row: T) => (string | number | null)[],
+ ): void {
+ const BATCH = 80;
+ const tuple = `(${columns.map(() => '?').join(', ')})`;
+ const sqlFor = (n: number) =>
+ `INSERT INTO ${table} (${columns.join(', ')}) VALUES ${Array.from({ length: n }, () => tuple).join(', ')}`;
+ const full = rows.length >= BATCH ? this.stmt(sqlFor(BATCH)) : null;
+ let i = 0;
+ for (; full && i + BATCH <= rows.length; i += BATCH) {
+ const params: (string | number | null)[] = [];
+ for (let k = i; k < i + BATCH; k++) params.push(...values(rows[k]!));
+ full.run(...params);
+ }
+ if (i < rows.length) {
+ const rest = rows.slice(i);
+ this.db.prepare(sqlFor(rest.length)).run(...rest.flatMap(values));
+ }
+ }
+
+ close(): void {
+ this.statements.clear();
+ this.db.close();
+ }
+
+ // ---- repositories -------------------------------------------------------
+
+ private toRepo(r: Row): StoredRepository {
+ return {
+ id: r.id as string,
+ name: r.name as string,
+ sourceKind: r.source_kind as StoredRepository['sourceKind'],
+ origin: r.origin as string,
+ branch: (r.branch as string | null) ?? null,
+ subdirectory: (r.subdirectory as string | null) ?? null,
+ rootPath: r.root_path as string,
+ managedSource: !!r.managed_source,
+ status: r.status as StoredRepository['status'],
+ createdAt: r.created_at as string,
+ lastIndexedAt: (r.last_indexed_at as string | null) ?? null,
+ fileCount: Number(r.file_count),
+ symbolCount: Number(r.symbol_count),
+ relationshipCount: Number(r.relationship_count),
+ error: (r.error as string | null) ?? null,
+ };
+ }
+
+ listRepositories(): StoredRepository[] {
+ return (this.stmt('SELECT * FROM repositories ORDER BY created_at DESC').all() as Row[]).map((r) =>
+ this.toRepo(r),
+ );
+ }
+
+ getRepository(id: string): StoredRepository | null {
+ const r = this.stmt('SELECT * FROM repositories WHERE id = ?').get(id) as Row | undefined;
+ return r ? this.toRepo(r) : null;
+ }
+
+ insertRepository(repo: StoredRepository): void {
+ this.stmt(
+ `INSERT INTO repositories (id, name, source_kind, origin, branch, subdirectory, root_path, managed_source, status, created_at, last_indexed_at, file_count, symbol_count, relationship_count, error)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ ).run(
+ repo.id,
+ repo.name,
+ repo.sourceKind,
+ repo.origin,
+ repo.branch,
+ repo.subdirectory,
+ repo.rootPath,
+ repo.managedSource ? 1 : 0,
+ repo.status,
+ repo.createdAt,
+ repo.lastIndexedAt,
+ repo.fileCount,
+ repo.symbolCount,
+ repo.relationshipCount,
+ repo.error,
+ );
+ }
+
+ updateRepository(
+ id: string,
+ patch: Partial<
+ Pick<
+ StoredRepository,
+ | 'status'
+ | 'lastIndexedAt'
+ | 'fileCount'
+ | 'symbolCount'
+ | 'relationshipCount'
+ | 'error'
+ | 'name'
+ | 'branch'
+ >
+ >,
+ ): void {
+ const map: Record = {
+ status: 'status',
+ lastIndexedAt: 'last_indexed_at',
+ fileCount: 'file_count',
+ symbolCount: 'symbol_count',
+ relationshipCount: 'relationship_count',
+ error: 'error',
+ name: 'name',
+ branch: 'branch',
+ };
+ const sets: string[] = [];
+ const values: (string | number | null)[] = [];
+ for (const [k, v] of Object.entries(patch)) {
+ if (v === undefined || !map[k]) continue;
+ sets.push(`${map[k]} = ?`);
+ values.push(v as string | number | null);
+ }
+ if (!sets.length) return;
+ this.db.prepare(`UPDATE repositories SET ${sets.join(', ')} WHERE id = ?`).run(...values, id);
+ }
+
+ deleteRepository(id: string): void {
+ this.transaction(() => {
+ for (const t of ['nodes', 'edges', 'file_facts'])
+ this.db.prepare(`DELETE FROM ${t} WHERE repo_id = ?`).run(id);
+ this.db.prepare('DELETE FROM sessions WHERE repo_id = ?').run(id);
+ this.db.prepare('DELETE FROM repositories WHERE id = ?').run(id);
+ });
+ }
+
+ getAnalysis(id: string): T | null {
+ const r = this.stmt('SELECT analysis FROM repositories WHERE id = ?').get(id) as Row | undefined;
+ return r?.analysis ? (JSON.parse(r.analysis as string) as T) : null;
+ }
+
+ setAnalysis(id: string, analysis: unknown): void {
+ this.stmt('UPDATE repositories SET analysis = ? WHERE id = ?').run(JSON.stringify(analysis), id);
+ }
+
+ // ---- graph --------------------------------------------------------------
+
+ saveGraph(repoId: string, graph: Pick): void {
+ this.transaction(() => {
+ this.stmt('DELETE FROM nodes WHERE repo_id = ?').run(repoId);
+ this.stmt('DELETE FROM edges WHERE repo_id = ?').run(repoId);
+ // Rows are written in multi-row statements: far fewer calls into SQLite than one per row.
+ this.bulkInsert('nodes', ['repo_id', 'id', 'type', 'name', 'file_path', 'data'], graph.nodes, (n) => [
+ repoId,
+ n.id,
+ n.type,
+ n.name,
+ n.filePath,
+ JSON.stringify(n),
+ ]);
+ this.bulkInsert('edges', ['repo_id', 'id', 'source', 'target', 'type', 'data'], graph.edges, (e) => [
+ repoId,
+ e.id,
+ e.source,
+ e.target,
+ e.type,
+ JSON.stringify(e),
+ ]);
+ });
+ }
+
+ /**
+ * Loads a graph. With `types`, only nodes of those types are returned and
+ * edges are limited to those between returned nodes (progressive loading).
+ */
+ loadGraph(repoId: string, opts: { types?: readonly NodeType[] } = {}): KnowledgeGraph {
+ let nodes: GraphNode[];
+ if (opts.types?.length) {
+ const marks = opts.types.map(() => '?').join(',');
+ nodes = (
+ this.db
+ .prepare(`SELECT data FROM nodes WHERE repo_id = ? AND type IN (${marks})`)
+ .all(repoId, ...opts.types) as Row[]
+ ).map((r) => JSON.parse(r.data as string) as GraphNode);
+ } else {
+ nodes = (this.stmt('SELECT data FROM nodes WHERE repo_id = ?').all(repoId) as Row[]).map(
+ (r) => JSON.parse(r.data as string) as GraphNode,
+ );
+ }
+ let edges = (this.stmt('SELECT data FROM edges WHERE repo_id = ?').all(repoId) as Row[]).map(
+ (r) => JSON.parse(r.data as string) as GraphEdge,
+ );
+ if (opts.types?.length) {
+ const ids = new Set(nodes.map((n) => n.id));
+ edges = edges.filter((e) => ids.has(e.source) && ids.has(e.target));
+ }
+ const analysis = this.getAnalysis<{ unresolvedCount?: number; generatedAt?: string }>(repoId);
+ return {
+ schemaVersion: SCHEMA_VERSION,
+ repositoryId: repoId,
+ generatedAt: analysis?.generatedAt ?? new Date().toISOString(),
+ nodes,
+ edges,
+ stats: computeStats(nodes, edges, analysis?.unresolvedCount ?? 0),
+ };
+ }
+
+ /** Nodes whose members were left out by a typed load, e.g. the symbols of one file. */
+ loadNodesByFile(repoId: string, filePath: string): { nodes: GraphNode[]; edges: GraphEdge[] } {
+ const nodes = (
+ this.stmt('SELECT data FROM nodes WHERE repo_id = ? AND file_path = ?').all(repoId, filePath) as Row[]
+ ).map((r) => JSON.parse(r.data as string) as GraphNode);
+ const edges = new Map();
+ const bySource = this.stmt('SELECT data FROM edges WHERE repo_id = ? AND source = ?');
+ const byTarget = this.stmt('SELECT data FROM edges WHERE repo_id = ? AND target = ?');
+ for (const n of nodes) {
+ for (const r of [...(bySource.all(repoId, n.id) as Row[]), ...(byTarget.all(repoId, n.id) as Row[])]) {
+ const e = JSON.parse(r.data as string) as GraphEdge;
+ edges.set(e.id, e);
+ }
+ }
+ return { nodes, edges: [...edges.values()] };
+ }
+
+ countGraph(repoId: string): { nodes: number; edges: number } {
+ const n = this.stmt('SELECT COUNT(*) AS c FROM nodes WHERE repo_id = ?').get(repoId) as Row;
+ const e = this.stmt('SELECT COUNT(*) AS c FROM edges WHERE repo_id = ?').get(repoId) as Row;
+ return { nodes: Number(n.c), edges: Number(e.c) };
+ }
+
+ // ---- cached per-file analysis ------------------------------------------
+
+ saveFacts(repoId: string, entries: CachedFacts[]): void {
+ this.transaction(() => {
+ this.stmt('DELETE FROM file_facts WHERE repo_id = ?').run(repoId);
+ this.bulkInsert('file_facts', ['repo_id', 'path', 'hash', 'facts'], entries, (e) => [
+ repoId,
+ e.path,
+ e.hash,
+ JSON.stringify(e.facts),
+ ]);
+ });
+ }
+
+ loadFacts(repoId: string): Map> {
+ const out = new Map>();
+ for (const r of this.stmt('SELECT path, hash, facts FROM file_facts WHERE repo_id = ?').all(
+ repoId,
+ ) as Row[])
+ out.set(r.path as string, {
+ path: r.path as string,
+ hash: r.hash as string,
+ facts: JSON.parse(r.facts as string) as T,
+ });
+ return out;
+ }
+
+ clearFacts(repoId?: string): void {
+ if (repoId) this.stmt('DELETE FROM file_facts WHERE repo_id = ?').run(repoId);
+ else this.db.exec('DELETE FROM file_facts');
+ }
+
+ // ---- sessions, settings, credentials -----------------------------------
+
+ getSession(repoId: string): SessionState | null {
+ const r = this.stmt('SELECT data FROM sessions WHERE repo_id = ?').get(repoId) as Row | undefined;
+ return r ? (JSON.parse(r.data as string) as SessionState) : null;
+ }
+
+ saveSession(repoId: string, session: SessionState): void {
+ this.stmt(
+ 'INSERT INTO sessions (repo_id, data) VALUES (?, ?) ON CONFLICT(repo_id) DO UPDATE SET data = excluded.data',
+ ).run(repoId, JSON.stringify(session));
+ }
+
+ getSettings(): Settings {
+ const r = this.stmt('SELECT data FROM settings WHERE id = 1').get() as Row | undefined;
+ if (!r) return defaultSettings();
+ const parsed = SettingsSchema.safeParse(JSON.parse(r.data as string));
+ return parsed.success ? parsed.data : defaultSettings();
+ }
+
+ saveSettings(settings: Settings): void {
+ this.stmt(
+ 'INSERT INTO settings (id, data) VALUES (1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data',
+ ).run(JSON.stringify(settings));
+ }
+
+ getCredential(name: string): string | null {
+ const r = this.stmt('SELECT value FROM credentials WHERE name = ?').get(name) as Row | undefined;
+ return r ? (r.value as string) : null;
+ }
+
+ setCredential(name: string, encryptedValue: string): void {
+ this.stmt(
+ 'INSERT INTO credentials (name, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at',
+ ).run(name, encryptedValue, new Date().toISOString());
+ }
+
+ deleteCredential(name: string): void {
+ this.stmt('DELETE FROM credentials WHERE name = ?').run(name);
+ }
+
+ listCredentials(): { name: string; updatedAt: string }[] {
+ return (this.stmt('SELECT name, updated_at FROM credentials ORDER BY name').all() as Row[]).map((r) => ({
+ name: r.name as string,
+ updatedAt: r.updated_at as string,
+ }));
+ }
+
+ /** Removes every repository, graph, cache, session and credential. Settings are kept unless asked. */
+ deleteAllData(includeSettings = false): void {
+ this.transaction(() => {
+ for (const t of ['nodes', 'edges', 'file_facts', 'sessions', 'repositories', 'credentials'])
+ this.db.exec(`DELETE FROM ${t}`);
+ if (includeSettings) this.db.exec('DELETE FROM settings');
+ });
+ this.db.exec('VACUUM');
+ }
+}
diff --git a/packages/graph-layout/package.json b/packages/graph-layout/package.json
new file mode 100644
index 0000000..6d24817
--- /dev/null
+++ b/packages/graph-layout/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "@vectora/graph-layout",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "license": "MIT",
+ "exports": {
+ ".": "./src/index.ts"
+ },
+ "dependencies": {
+ "@vectora/graph-schema": "*",
+ "d3-force-3d": "^3.0.6"
+ }
+}
diff --git a/packages/graph-layout/src/d3-force-3d.d.ts b/packages/graph-layout/src/d3-force-3d.d.ts
new file mode 100644
index 0000000..2410243
--- /dev/null
+++ b/packages/graph-layout/src/d3-force-3d.d.ts
@@ -0,0 +1,53 @@
+declare module 'd3-force-3d' {
+ export interface SimNode {
+ index?: number;
+ x?: number;
+ y?: number;
+ z?: number;
+ vx?: number;
+ vy?: number;
+ vz?: number;
+ fx?: number | null;
+ fy?: number | null;
+ fz?: number | null;
+ }
+ export interface Force {
+ (alpha: number): void;
+ initialize?: (nodes: N[], ...rest: unknown[]) => void;
+ }
+ export interface Simulation {
+ tick(iterations?: number): this;
+ stop(): this;
+ alpha(value: number): this;
+ alphaDecay(value: number): this;
+ velocityDecay(value: number): this;
+ force(name: string, force: unknown): this;
+ nodes(): N[];
+ }
+ export function forceSimulation(nodes: N[], numDimensions?: 1 | 2 | 3): Simulation;
+ export function forceLink(
+ links: { source: number | N; target: number | N }[],
+ ): {
+ distance(d: number | ((l: unknown) => number)): ReturnType>;
+ strength(s: number | ((l: unknown) => number)): ReturnType>;
+ iterations(n: number): ReturnType>;
+ };
+ export function forceManyBody(): {
+ strength(s: number | ((n: unknown) => number)): ReturnType;
+ distanceMax(d: number): ReturnType