From 92b501876f241c01eb4567a16fc5fad6b7f6c9c7 Mon Sep 17 00:00:00 2001 From: John Kupchanko Date: Wed, 2 Sep 2026 12:14:21 -0700 Subject: [PATCH 1/2] Rebuild the backend on Qdrant Cloud Inference, drop Railway Railway existed only so the backend could load UniXcoder and embed the query itself. Qdrant Cloud Inference does that inside the cluster, so the query goes out as text and comes back ranked, and the backend is three TypeScript functions with no dependencies in the same Vercel project as the frontend. Gone with it: the Dockerfile, docker-compose.yaml, railway.json, requirements.txt and the whole code_search package. UniXcoder is not in the Cloud Inference catalog and cannot be added, so this forced replacing the encoder. bench/bakeoff.py scores ten configurations through the real production path; mxbai-embed-large-v1 with BM25 fused by RRF won. SPLADE matches BM25 on docstring queries, loses on paraphrases and runs three times slower. DBSF fusion was tried and rejected. One fix is unrelated to the migration and is the largest single win here. The merge step sorted results by how many overlapping snippet ranges each one had, so a result with two highlighted ranges outranked the actual best match with none. Removing that took docstring recall@1 from 0.620 to 0.887. It is inherited behaviour: measured directly the old demo's own search scores 0.730 while its deployment returns 0.567. Against the previous deployment on the same 413 queries: docstring recall@1 0.567 to 0.883, MRR 0.713 to 0.928, p50 178ms to 148ms, cold start 2033ms to 775ms. p95 is worse, 186ms to 205ms, and 46ms of that spread is the dense model's inference call against 1.4ms for the BM25 leg. The frontend is unchanged apart from wording that named the old encoders. The CSS file is byte-identical, the per-result JSON keys are identical, and the corpus and indexed commit are the same, so every result link resolves to the same code. --- .dockerignore | 3 - .env.example | 43 +- .github/dependabot.yml | 14 +- .github/workflows/ci.yml | 41 + .github/workflows/deploy.yml | 49 - .github/workflows/index-latest.yml | 110 +- .gitignore | 173 +- DEPLOY.md | 188 +- Dockerfile | 25 - README.md | 620 +++-- REVIEW-RESPONSE.md | 155 ++ api/_lib/cache.ts | Bin 0 -> 2485 bytes api/_lib/config.ts | 42 + api/_lib/merge.ts | 92 + api/_lib/qdrant.ts | 88 + api/_lib/search.ts | 76 + api/file.ts | 45 + api/health.ts | 48 + api/search.ts | 60 + bench/bakeoff.py | 259 ++ bench/common.py | 190 ++ bench/fusion_check.py | 164 ++ bench/index_cost.py | 111 + bench/latency.py | 160 ++ bench/quality.py | 156 ++ bench/results/bakeoff.json | 216 ++ bench/results/fusion_check.json | 2167 +++++++++++++++++ bench/results/index_cost.json | 25 + bench/results/latency.json | 36 + bench/results/quality.json | 84 + bench/results/truncation.json | 37 + bench/significance.py | 110 + bench/truncation.py | 197 ++ code_search/__init__.py | 0 code_search/config.py | 83 - code_search/get_file.py | 34 - code_search/index/__init__.py | 0 code_search/index/file_uploader.py | 57 - code_search/index/helper.py | 146 -- code_search/index/upload_code.py | 100 - code_search/index/upload_signatures.py | 78 - code_search/model/__init__.py | 0 code_search/model/encoder.py | 29 - code_search/model/unixcoder.py | 293 --- code_search/postprocessing.py | 97 - code_search/searcher.py | 86 - code_search/service.py | 171 -- docker-compose.yaml | 9 - frontend/.env.example | 6 - frontend/index.html | 4 +- frontend/src/api/axios.ts | 13 +- frontend/src/api/search.ts | 38 +- .../src/components/CustomHeader/index.tsx | 4 +- .../components/MainSection/Main.module.css | 7 +- frontend/src/components/MainSection/index.tsx | 19 +- frontend/vercel.json | 9 - frontend/vite.config.ts | 5 +- images/architecture-diagram.png | Bin 273123 -> 0 bytes images/architecture.svg | 59 + indexer/build.py | 361 +++ indexer/paths.py | 11 + .../prepare}/convert_lsif_index.py | 7 +- .../prepare}/files_to_json.py | 7 +- indexer/qdrant_rest.py | 121 + {code_search/index => indexer}/textifier.py | 0 package-lock.json | 1823 ++++++++++++++ package.json | 14 + pyproject.toml | 23 - railway.json | 13 - requirements.txt | 10 - test/merge.test.mjs | 106 + tools/download_and_index.sh | 11 +- tools/index_qdrant.sh | 32 +- tools/migrate_to_qdrant_cloud.py | 77 - tsconfig.json | 16 + vercel.json | 20 + 76 files changed, 7833 insertions(+), 1950 deletions(-) delete mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 Dockerfile create mode 100644 REVIEW-RESPONSE.md create mode 100644 api/_lib/cache.ts create mode 100644 api/_lib/config.ts create mode 100644 api/_lib/merge.ts create mode 100644 api/_lib/qdrant.ts create mode 100644 api/_lib/search.ts create mode 100644 api/file.ts create mode 100644 api/health.ts create mode 100644 api/search.ts create mode 100644 bench/bakeoff.py create mode 100644 bench/common.py create mode 100644 bench/fusion_check.py create mode 100644 bench/index_cost.py create mode 100644 bench/latency.py create mode 100644 bench/quality.py create mode 100644 bench/results/bakeoff.json create mode 100644 bench/results/fusion_check.json create mode 100644 bench/results/index_cost.json create mode 100644 bench/results/latency.json create mode 100644 bench/results/quality.json create mode 100644 bench/results/truncation.json create mode 100644 bench/significance.py create mode 100644 bench/truncation.py delete mode 100644 code_search/__init__.py delete mode 100644 code_search/config.py delete mode 100644 code_search/get_file.py delete mode 100644 code_search/index/__init__.py delete mode 100644 code_search/index/file_uploader.py delete mode 100644 code_search/index/helper.py delete mode 100644 code_search/index/upload_code.py delete mode 100644 code_search/index/upload_signatures.py delete mode 100644 code_search/model/__init__.py delete mode 100644 code_search/model/encoder.py delete mode 100644 code_search/model/unixcoder.py delete mode 100644 code_search/postprocessing.py delete mode 100644 code_search/searcher.py delete mode 100644 code_search/service.py delete mode 100644 docker-compose.yaml delete mode 100644 frontend/.env.example delete mode 100644 frontend/vercel.json delete mode 100644 images/architecture-diagram.png create mode 100644 images/architecture.svg create mode 100644 indexer/build.py create mode 100644 indexer/paths.py rename {code_search/index => indexer/prepare}/convert_lsif_index.py (95%) rename {code_search/index => indexer/prepare}/files_to_json.py (90%) create mode 100644 indexer/qdrant_rest.py rename {code_search/index => indexer}/textifier.py (100%) create mode 100644 package-lock.json create mode 100644 package.json delete mode 100644 pyproject.toml delete mode 100644 railway.json delete mode 100644 requirements.txt create mode 100644 test/merge.test.mjs delete mode 100644 tools/migrate_to_qdrant_cloud.py create mode 100644 tsconfig.json create mode 100644 vercel.json diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index dfff83a..0000000 --- a/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -data/ -frontend/node_modules/ -venv/ diff --git a/.env.example b/.env.example index b7340a2..e0b69d3 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,42 @@ -# Backend runtime config. -# Local: copy to .env. Railway: set these as service variables. +# Runtime config. Local: copy to .env.local and run `vercel dev`. +# Production: set these as Vercel project environment variables. +# +# There is exactly one service to configure. The frontend, the API functions and +# the embedding models all live behind these two variables. -# Qdrant instance URL. Point at Qdrant Cloud for production. -QDRANT_URL=https://your-cluster-id.aws.cloud.qdrant.io:6333 +# Qdrant Cloud cluster URL, with the port. +QDRANT_URL=https://your-cluster-id.region.aws.cloud.qdrant.io:6333 -# Qdrant Cloud API key. Empty for self-hosted local instances. +# Qdrant Cloud API key. QDRANT_API_KEY= -# Comma-separated allowlist of frontend origins that may call this API. -# Example: https://code-search.vercel.app,https://code-search-git-main.vercel.app -CORS_ORIGINS=https://code-search.vercel.app +# Collections. Defaults match what indexer/build.py creates. +QDRANT_CODE_COLLECTION=code-snippets-cloud +QDRANT_NLU_COLLECTION=code-signatures-cloud +QDRANT_FILE_COLLECTION=code-files-cloud -# Uvicorn worker count. 1 is fine unless you need concurrent requests. -WORKERS=1 +# Models, both served from inside the cluster by Qdrant Cloud Inference. +# Changing these means rebuilding the collections: the dense dimension is part +# of the collection config, and the sparse leg's statistics are per-collection. +QDRANT_DENSE_MODEL=mixedbread-ai/mxbai-embed-large-v1 +QDRANT_SPARSE_MODEL=Qdrant/bm25 + +# Candidates each leg of the hybrid query contributes before fusion. +PREFETCH_LIMIT=100 + +# Indexing only. Cut every document to this many characters before sending it to +# be embedded; 0 disables it. +# +# Qdrant Cloud Inference truncates at 512 tokens for every model, but all-MiniLM-L6-v2 +# has a published max_seq_length of 256 and degrades when fed past it. On this +# corpus that is worth 0.863 vs 0.937 docstring recall@10 - see +# bench/truncation.py. 700 characters is roughly 256 tokens of Rust. +# +# Set to 0 for a model whose own limit is 512, such as mxbai-embed-large-v1. +INDEX_CHAR_BUDGET=0 # Commit of qdrant/qdrant the collections were built from. Result links carry # line numbers, so they only land on the right code when resolved against this. # The indexing run prints the SHA at the end of its log. Defaults to `master`, -# which is what the links used before and drifts as the source moves. +# which drifts as the source moves. INDEXED_COMMIT= diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c3bdd4c..1542229 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,12 @@ +# No pip or docker ecosystems here: the indexer and the benchmarks are stdlib, +# and there is no container to keep patched. What is left is npm in two places +# and the actions. version: 2 updates: - - package-ecosystem: "pip" - directory: "/" + - package-ecosystem: "npm" + directories: + - "/" + - "/frontend" schedule: interval: "weekly" open-pull-requests-limit: 5 @@ -17,11 +22,6 @@ updates: - "patch" patterns: - "*" - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..090820c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + # The API functions and the frontend are typechecked separately: they have + # different tsconfigs, and the API's has no DOM-heavy React types in it. + - name: Install API dev deps + run: npm install + - name: Typecheck API + run: npm run typecheck + - name: Test API + run: npm test + - name: Install frontend + run: npm --prefix frontend ci + - name: Typecheck and build frontend + run: npm --prefix frontend run build + - name: Lint frontend + run: npm --prefix frontend run lint + + # The bench scripts are stdlib-only on purpose; this catches a syntax error + # before someone discovers it four hours into an indexing run. + python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Compile + run: python -m compileall -q bench indexer diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 73a2dab..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,49 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Deploy - -# Controls when the action will run. Triggers the workflow on push or pull request -# events but only for the master branch -on: - push: - branches: [ master ] - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - # This workflow contains a single job called "build" - build: - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Build backend - run: | - cd $GITHUB_WORKSPACE - docker build -t qdrant/code-search-web:${{ github.sha }} . - docker save -o code-search-web.tar qdrant/code-search-web:${{ github.sha }} - chmod 666 code-search-web.tar - ls -al . - - name: copy data with ssh - uses: appleboy/scp-action@master - with: - host: ${{ secrets.DEPLOY_HOST }} - username: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - source: "code-search-web.tar" - target: "deployment/data" - - - name: run images - uses: fifsky/ssh-action@master - with: - command: | - docker load -i deployment/data/code-search-web.tar; - docker images; - docker kill code-search-web || true; - docker rm code-search-web || true; - docker run -d -p 8066:8000 -e QDRANT_URL="${{ secrets.QDRANT_URL }}" -e QDRANT_API_KEY="${{ secrets.QDRANT_API_KEY }}" --restart unless-stopped --network=qdrant-net --name code-search-web qdrant/code-search-web:${{ github.sha }}; - host: ${{ secrets.DEPLOY_HOST }} - user: ${{ secrets.DEPLOY_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} diff --git a/.github/workflows/index-latest.yml b/.github/workflows/index-latest.yml index 11d37ec..f592d60 100644 --- a/.github/workflows/index-latest.yml +++ b/.github/workflows/index-latest.yml @@ -1,8 +1,8 @@ name: Index Qdrant source # Trigger manually. No schedule here: GitHub disables scheduled workflows in -# forks, which left this one stuck in `disabled_fork` and unrunnable. A demo -# doesn't need nightly reindexing anyway, and the run takes about six hours. +# forks, which left the old one stuck in `disabled_fork` and unrunnable. A demo +# doesn't need nightly reindexing anyway. on: workflow_dispatch: @@ -11,53 +11,65 @@ env: jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install minimal stable - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - - name: Set up Python 3.10 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: '3.10' - - name: Install dependencies - run: | - pip install -r requirements.txt - - name: Add rust analyzer - run: rustup component add rust-analyzer - # Shape-check the secrets before spending six hours on the indexing run. - # Reports length and word count rather than the values themselves, which is - # enough to spot the usual breakage: prompt text or a stray newline captured - # along with the value when it was pasted in. - - name: Check secrets - env: - QDRANT_URL: ${{ secrets.QDRANT_URL }} - QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }} - run: | - fail=0 - for name in QDRANT_URL QDRANT_API_KEY; do - value="${!name}" - words=$(printf '%s' "$value" | wc -w | tr -d ' ') - printf '%s: %s chars, %s word(s)\n' "$name" "${#value}" "$words" - if [ -z "$value" ]; then echo " -> not set"; fail=1; fi - if [ "$words" -gt 1 ]; then echo " -> contains whitespace, so it was pasted with extra text"; fail=1; fi - done - case "$QDRANT_URL" in https://*) ;; *) echo 'QDRANT_URL does not start with https://'; fail=1;; esac - # Qdrant Cloud keys are JWTs, but self-hosted keys are arbitrary strings, - # so a non-JWT shape is worth a note in the log rather than a failed run. - case "$QDRANT_API_KEY" in eyJ*) ;; *) echo 'note: QDRANT_API_KEY is not JWT-shaped, fine for self-hosted Qdrant';; esac - [ "$fail" -eq 0 ] || { echo 'Re-set the offending secret with: gh secret set --repo qdrant-labs/demo-code-search'; exit 1; } - echo 'Both secrets look well-formed.' - # Passed through env rather than interpolated into the command line: a value - # containing a space used to split the assignment and abort the run at - # `secret: command not found` before any indexing happened. - - name: Run indexing - env: - QDRANT_URL: ${{ secrets.QDRANT_URL }} - QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }} - run: bash -x tools/download_and_index.sh + - uses: actions/checkout@v4 + - name: Install minimal stable + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Add rust analyzer + run: rustup component add rust-analyzer + + # Shape-check the secrets before spending hours on the indexing run. + # Reports length and word count rather than the values themselves, which + # is enough to spot the usual breakage: prompt text or a stray newline + # captured along with the value when it was pasted in. + - name: Check secrets + env: + QDRANT_URL: ${{ secrets.QDRANT_URL }} + QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }} + run: | + fail=0 + for name in QDRANT_URL QDRANT_API_KEY; do + value="${!name}" + words=$(printf '%s' "$value" | wc -w | tr -d ' ') + printf '%s: %s chars, %s word(s)\n' "$name" "${#value}" "$words" + if [ -z "$value" ]; then echo " -> not set"; fail=1; fi + if [ "$words" -gt 1 ]; then echo " -> contains whitespace, so it was pasted with extra text"; fail=1; fi + done + case "$QDRANT_URL" in https://*) ;; *) echo 'QDRANT_URL does not start with https://'; fail=1;; esac + case "$QDRANT_API_KEY" in eyJ*) ;; *) echo 'note: QDRANT_API_KEY is not JWT-shaped, fine for self-hosted Qdrant';; esac + [ "$fail" -eq 0 ] || { echo 'Re-set the offending secret.'; exit 1; } + echo 'Both secrets look well-formed.' + + # Qdrant Cloud Inference has to be on for the cluster, or every upsert fails with + # "Unsupported model" after the corpus has already been built. Two seconds + # here against hours of wasted run. + - name: Check Qdrant Cloud Inference is enabled + env: + QDRANT_URL: ${{ secrets.QDRANT_URL }} + QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }} + run: | + body='{"query":{"text":"probe","model":"sentence-transformers/all-MiniLM-L6-v2"},"limit":1}' + out=$(curl -sS -X POST -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \ + "$QDRANT_URL/collections/does-not-exist/points/query" -d "$body") + echo "$out" + case "$out" in + *"Unsupported model"*|*"Expected some form of vector"*) + echo 'Qdrant Cloud Inference is not serving this model on this cluster.' + echo 'Enable it on the Inference tab of the cluster page in the Cloud Console.' + exit 1;; + esac + echo 'Inference reachable.' + + - name: Run indexing + env: + QDRANT_URL: ${{ secrets.QDRANT_URL }} + QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }} + run: bash -x tools/download_and_index.sh diff --git a/.gitignore b/.gitignore index c4a87f3..b1f4ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,168 +1,11 @@ -# Byte-compiled / optimized / DLL files +node_modules/ __pycache__/ -*.py[cod] -*$py.class - -.idea/ - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# Do not include auto-generated setup.py -/setup.py - -.DS_Store - -# Generated indexing artifacts (regenerated by tools/download_and_index.sh) -/data/* -!/data/.keep - -# Local IDE / harness config -.claude/ - -# Local env overrides (both root and frontend) +*.pyc .env .env.local -/frontend/.env -/frontend/.env.local - -# Vite build output -/frontend/dist/ - - -# Dependencies are installed from requirements.txt everywhere: the Dockerfile, -# the indexing workflow, and DEPLOY.md. A poetry.lock alongside it drifted out -# of sync with pyproject.toml and only produced advisories for packages that -# were never installed. -poetry.lock +.vercel +.test-build/ +data/* +!data/.keep +indexer/.state/ +bench/results/*.log diff --git a/DEPLOY.md b/DEPLOY.md index b6d6642..5f263b8 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,104 +1,154 @@ -# Deploying to Vercel + Railway + Qdrant Cloud +# Deploying -This repo is set up for a three-service split: +Two accounts, one deployment, two environment variables. | Piece | Where | Cost | |---|---|---| -| Frontend (React/Vite static build) | Vercel | Free | -| Backend (FastAPI + UniXcoder) | Railway | ~$5–10/mo | -| Vector database | Qdrant Cloud | Free tier (1 GB) | +| Frontend and API (one Vercel project) | Vercel | Free (Hobby) | +| Vector search engine and embedding models | Qdrant Cloud | Free tier (1 GB) | -Total setup time: ~30 minutes plus the migration copy (a few minutes on top). +The previous build needed a third: Railway, running a container with torch, +transformers and sentence-transformers in it, because the backend embedded the +query itself. Qdrant Cloud Inference does that inside the cluster, so there is +nothing left for that container to do. The API is four small TypeScript +functions that post JSON. --- -## 1. Create a Qdrant Cloud cluster +## 1. Qdrant Cloud -1. Sign up at https://cloud.qdrant.io. -2. Create a free-tier cluster (1 GB is plenty for this dataset). -3. Copy the cluster URL and generate an API key. You will need both below. +1. Create a cluster at https://cloud.qdrant.io. The free 1 GB tier holds this + dataset with room to spare. +2. Open the cluster, go to the **Inference** tab, and check it is enabled. + Clusters created after 7 July 2025 have it on already. Enabling it on an + older cluster **restarts the cluster**, so do that before you point anything + at it, not after. +3. Copy the cluster URL (including `:6333`) and create an API key. -## 2. Migrate the collections into Qdrant Cloud +**Server minimum: Qdrant 1.10.** The search is a `prefetch` with two legs and a +`fusion` step, which the Query API gained in 1.10. An older server rejects the +request outright rather than degrading, so this fails loudly rather than +quietly. Qdrant Cloud Inference is a separate requirement on top of the version: +a 1.19 cluster with inference switched off still cannot embed the query. -Make sure your local Qdrant container is running and has these collections: +Check inference is actually answering before you spend hours indexing: -- `code-files` (file contents used by the file endpoint) -- `code-signatures` (MiniLM embeddings of function signatures) -- `code-snippets-unixcoder` (UniXcoder embeddings; still building on first run) +```bash +curl -sS -X POST "$QDRANT_URL/collections/does-not-exist/points/query" \ + -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \ + -d '{"query":{"text":"probe","model":"sentence-transformers/all-MiniLM-L6-v2"},"limit":1}' +``` + +`Collection 'does-not-exist' doesn't exist` is the answer you want: the request +reached the engine, which means the model resolved. `Unsupported model` or +`Expected some form of vector` means inference is off for this cluster. + +## 2. Build the Index + +Either build from source, which needs Rust, rust-analyzer and Docker: + +```bash +export QDRANT_URL="https://your-cluster-id.region.aws.cloud.qdrant.io:6333" +export QDRANT_API_KEY="..." +bash tools/download_and_index.sh +``` -Then copy them into the cloud cluster: +Or, if you are migrating from an existing deployment of the older demo, copy the +corpus straight out of its collections and re-embed it: ```bash -export SRC_URL=http://localhost:6333 -export DST_URL=https://your-cluster-id.aws.cloud.qdrant.io:6333 -export DST_API_KEY=your-cloud-api-key -python -m tools.migrate_to_qdrant_cloud +python indexer/build.py --source qdrant ``` -If a collection is not yet ready locally (for example the UniXcoder run is -still going), the script prints `skip` for it and moves on. Re-run once -it's ready. +That reads `code-snippets-unixcoder`, `code-signatures` and `code-files` and +writes `code-snippets-cloud`, `code-signatures-cloud` and `code-files-cloud`. +The old collections are only ever read, so the old demo keeps working while this +one is built, and both can run side by side until you switch the domain over. + +Runs are resumable. If one dies at hour two, run it again and it skips the +batches already acknowledged instead of paying to embed them twice. -## 3. Deploy the backend to Railway +Note the commit the index was built from. `tools/download_and_index.sh` prints it +at the end; it goes into `INDEXED_COMMIT` below, and without it every result link +resolves against a moving `master` and eventually points at the wrong lines. -1. Push this repo to GitHub. -2. In Railway, click **New Project → Deploy from GitHub Repo** and pick this - repo. Railway detects [Dockerfile](Dockerfile) and [railway.json](railway.json). -3. Set the following service variables in Railway: +## 3. Vercel + +1. **Add New → Project**, import this repo, leave the root directory at the + repository root. [vercel.json](vercel.json) builds the frontend into + `frontend/dist` and picks up the functions in [`api/`](api). +2. Add the environment variables: | Variable | Value | |---|---| - | `QDRANT_URL` | your Qdrant Cloud URL (e.g. `https://xxx.aws.cloud.qdrant.io:6333`) | - | `QDRANT_API_KEY` | your Qdrant Cloud API key | - | `CORS_ORIGINS` | your Vercel frontend URL (see step 4). Comma-separate multiple. | - | `WORKERS` | `1` (raise only if you upgrade RAM significantly) | - | `INDEXED_COMMIT` | the qdrant/qdrant SHA the collections were built from, printed at the end of the indexing log. Result links carry line numbers and only match at that commit; leave it unset and they resolve against `master`, drifting as the source moves. | + | `QDRANT_URL` | cluster URL including `:6333` | + | `QDRANT_API_KEY` | cluster API key | + | `INDEXED_COMMIT` | the qdrant/qdrant SHA the collections were built from | + + Everything else has a working default; see [.env.example](.env.example). +3. Deploy. + +There is no `VITE_API_URL` and no `CORS_ORIGINS`, because the app and the API +are served from one origin. If you find yourself adding either one back, the +deployment has split in two again. -4. Railway will build and expose a URL like `https://code-search-api.up.railway.app`. - Wait for the health check at `/api/health` to pass; first boot takes ~30–60s - because the UniXcoder model loads into memory. -5. Copy that URL — you'll paste it into Vercel next. +## 4. Verify -**Sizing note.** UniXcoder plus MiniLM want about 2 GB of RAM to be comfortable. -The free/Hobby "Starter" instance (512 MB) will OOM. Use Railway's **Hobby** -plan (2 GB) or higher. +```bash +curl -s https:///api/health | python -m json.tool +``` -## 4. Deploy the frontend to Vercel +It reports the point count of all three collections and the models in use, and +answers 503 if any collection is missing or empty. A bare `{"status":"ok"}` +would have passed on every day the old demo sat broken, so it does not do that. -1. In Vercel, click **Add New → Project** and import this repo. -2. Set the **Root Directory** to `frontend`. Vercel picks up - [vercel.json](frontend/vercel.json) automatically. -3. Under **Environment Variables**, add: +Then measure it, rather than assuming: - | Variable | Value | - |---|---| - | `VITE_API_URL` | your Railway backend URL, no trailing slash | +```bash +python bench/latency.py --target https:// +python bench/quality.py --target https:// +``` -4. Click **Deploy**. Vercel gives you a URL like - `https://code-search.vercel.app`. -5. Go back to Railway and update `CORS_ORIGINS` with that exact URL. Railway - redeploys automatically. +## Local Development -## 5. Verify +```bash +cp .env.example .env # fill in QDRANT_URL and QDRANT_API_KEY +npm install +npx vercel dev +``` + +`.env`, not `.env.local`: for a project that has not been linked with +`vercel link`, the CLI reads `.env` and leaves `.env.local` alone, and the +functions come up throwing `QDRANT_URL is not set`. Both are gitignored. -- Hit the frontend URL — you should see the hero. -- Run one of the demo queries. You should see results (semantic if UniXcoder - is loaded, "Warming Up" keyword mode otherwise). -- Click into a code card and hit "load more lines" — the file endpoint should - return 200s. +There is deliberately no `dev` script in `package.json`: `vercel dev` runs the +project's own `dev` script, so defining one as `vercel dev` makes it invoke +itself and refuse to start. + +`vercel dev` serves the frontend and the functions together on +http://127.0.0.1:3000, which is the same shape as production. If you would rather +have Vite's hot reload, run `npm --prefix frontend run dev` alongside it; the dev +server proxies `/api` to port 3000. ## Troubleshooting -- **Search returns 500 with a Qdrant 404 error**: the target collection was - not migrated. Re-run `python -m tools.migrate_to_qdrant_cloud`. -- **CORS error in browser console**: the frontend URL isn't in `CORS_ORIGINS` - on Railway. Add it (comma-separated for multiple). -- **Backend crashes on boot with OOM**: raise the Railway plan to at least - 2 GB, or switch to a MiniLM-only build (drop UniXcoder from `CodeSearcher`). -- **Slow first request**: normal. UniXcoder loads once per container, then - requests are fast. +**Search returns 503 "a Qdrant collection is missing".** The index was not +built, or `QDRANT_*_COLLECTION` points at a name that does not exist. `/api/health` +names which one. + +**Upserts fail with `Unsupported model`.** Qdrant Cloud Inference is off for the +cluster, or the model name is misspelled. Names are case-insensitive on the wire +but must otherwise match the Inference tab exactly. + +**Upserts fail with `Vector dimension error`.** The +collection was created for one dense model and is being filled with another. +Rebuild it with `--fresh`, or set `QDRANT_DENSE_MODEL` back. -## Custom domain +**File viewer 500s on every result.** The `path` payload index is missing. +`indexer/build.py` creates it; clusters with strict mode on refuse to filter an +unindexed field, and search keeps working meanwhile, which makes this look like a +frontend bug. -Both Vercel and Railway support custom domains. If you attach one to the -frontend, remember to update `CORS_ORIGINS` on Railway to include it. +**A function times out at 20 seconds.** That is `maxDuration` in +[vercel.json](vercel.json), and hitting it means Qdrant is not answering, not +that the search is slow. Check the cluster. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 98d0efe..0000000 --- a/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM node:22-alpine AS builder - -COPY frontend /frontend -WORKDIR /frontend - -RUN npm ci && npm run build - -FROM python:3.12-slim - -# Copy only requirements first so dependency installs are cached in their own layer -WORKDIR /code -COPY requirements.txt /code/ - -RUN pip install --no-cache-dir -r requirements.txt - -# Bake the pre-trained models into the image -RUN python -c 'from sentence_transformers import SentenceTransformer; SentenceTransformer("all-MiniLM-L6-v2");' -RUN python -c 'from transformers import RobertaTokenizer, RobertaModel, RobertaConfig; RobertaTokenizer.from_pretrained("microsoft/unixcoder-base") ; RobertaModel.from_pretrained("microsoft/unixcoder-base") ; RobertaConfig.from_pretrained("microsoft/unixcoder-base");' - -COPY . /code - -COPY --from=builder /frontend/dist /code/frontend/dist - -# Railway/Fly.io/Render pass a PORT env var; fall back to 8000 locally. -CMD uvicorn code_search.service:app --host 0.0.0.0 --port ${PORT:-8000} --workers ${WORKERS:-1} diff --git a/README.md b/README.md index 00578cd..488fdb9 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,100 @@ -# Code search with Qdrant +# Code Search with Qdrant -Developers need a code search tool that helps them find the right piece of code. In this README, we describe how -you can set up a tool that provides code results, in context. +Developers need a code search tool that helps them find the right piece of code. +This repository is that tool, built on [Qdrant](https://qdrant.tech), searching +the [Qdrant source](https://github.com/qdrant/qdrant), and deployed as two +things: a Vercel project and a Qdrant Cloud cluster. -## Online version +It is a rebuild of [qdrant/demo-code-search](https://github.com/qdrant/demo-code-search). +The interface is the same. What changed is everything behind it, and +[why](#why-this-was-rebuilt) is the interesting part. -See our code search tool "in action." Navigate to -**[https://code-search.qdrant.tech/](https://code-search.qdrant.tech/)**. We've prepopulated the demo with Qdrant -codebase. You can see the results, in context, even with relatively vague search terms. +## Online Version -The refreshed build in this repository is deployed at -**[https://demo-code-search-production.up.railway.app](https://demo-code-search-production.up.railway.app)**, -serving both the frontend and the API from one container. Until -`code-search.qdrant.tech` is pointed at it, the two run side by side against -separate Qdrant clusters — see [DEPLOY.md](DEPLOY.md). +The rebuild is deployed on Vercel. The previous build is still running on +Railway at +[demo-code-search-production.up.railway.app](https://demo-code-search-production.up.railway.app), +untouched, reading its own collections, so the two can be compared side by side. -## Prerequisites +`code-search.qdrant.tech` currently answers 404 and is not serving either of +them. -To run this demo on your own system, install and/or set up the following components: +## Why This Was Rebuilt -- [Docker](https://www.docker.com/) -- [Docker Compose](https://docs.docker.com/compose/) -- [Rust](https://www.rust-lang.org/learn/get-started) -- [rust-analyzer](https://rust-analyzer.github.io/) +The previous build needed three services to answer a search: -Docker and Docker Compose setup depends on your operating system. Please refer to the official documentation for -instructions on how to install them. Both Rust and rust-analyzer can be installed with the following commands: +- **Vercel** served the frontend. +- **Railway** ran a FastAPI container that held `torch`, `transformers`, + `sentence-transformers` and two models in memory, because the backend had to + turn the query into a vector before it could ask Qdrant anything. +- **Qdrant Cloud** stored the vectors. -```shell -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -rustup component add rust-analyzer -``` +Railway existed only for that middle step. Qdrant Cloud Inference does the +embedding inside the cluster, so the query goes out as text and comes back as +ranked results. That leaves nothing for the container to do, and the backend +collapses into three small TypeScript functions that post JSON and can sit in +the same Vercel project as the frontend. -## Description +Two vendors, one deployment, two environment variables. -You can set up [Qdrant](https://qdrant.tech) to help developers find the code they need, with context. Using semantic -search, developers can find the code samples that can help them do their day-to-day work, even with: +![Architecture](images/architecture.svg) -- Imprecise keywords -- Inexact names for functions, classes or variables -- Some other code snippets +### The Catch, and What It Cost -The demo uses [Qdrant source code](https://github.com/qdrant/qdrant) to build an end-to-end code search application that -helps you find the right piece of code, even if you have never contributed to the project. We implemented an end-to-end -process, including data chunking, indexing, and search. Code search is a very specific task in which the programming -language syntax matters as much as the function, class, variable names, and the docstring, describing what and why. -While the latter is more of a traditional natural language processing task, the former requires a specific approach. -Thus, we use the following neural encoders for our use cases: +`microsoft/unixcoder-base` is not in the Qdrant Cloud Inference catalog and cannot be +added to it. External models are reachable by prefix (`openai/`, `cohere/`, +`jinaai/`, `openrouter/`) but each one needs its own API key passed per request, +which is a third vendor by another name. So consolidating **forced replacing the +encoder**, and that made the model choice the whole migration rather than a +detail of it. -- `all-MiniLM-L6-v2` - one of the gold standard models for natural language processing -- `microsoft/unixcoder-base` - a model trained specifically on a code dataset +The replacement was picked by measurement, not by preference. See +[Benchmarks](#benchmarks). -### Chunking and indexing process +### What This Gives Up -Semantic search works best with _structured_ source code repositories, with good syntax, as well as best practices -as defined by the authoring team. If your code base needs help, start by dividing the code into chunks. Each -chunk should correspond to a specific function, struct, enum, or any other code structure that might be considered as a whole. +**You can no longer run the whole demo locally.** Qdrant Cloud Inference is a Qdrant +Cloud feature, so `docker run qdrant/qdrant` will not serve the models and the +collections cannot be built against a local instance. The previous build ran end +to end on a laptop with no accounts at all. This one needs a cluster, and the +free tier is what that costs. -There is a separate model-specific logic that extracts the most important parts of the code and converts them -into a format that the neural network can understand. Only then, the encoded representation is indexed in the Qdrant -collection, along with a JSON structure describing that snippet as a payload. +`docker-compose.yaml`, the `Dockerfile` and `requirements.txt` are gone with it. +Nothing replaced them, which is the point, but it does mean the offline path is +gone too. + +## Architecture -To that end, we work with the following models. The combination is the "best of both worlds." +- [`frontend/`](frontend): React app, unchanged from the previous build except + for the parts that named the old encoders. +- [`api/`](api): three Vercel functions. No dependencies at runtime; the whole + API is `fetch` and JSON. +- [`indexer/`](indexer): builds the collections. No model runtime, because the + cluster does the embedding. +- [`bench/`](bench): the measurements behind every number on this page. + Stdlib-only, so anyone can re-run them. -#### all-MiniLM-L6-v2 +Three collections, the same three as before: -Before the encoding, code is divided into chunks, but contrary to the traditional NLP challenges, it contains not only -the definition of the function or class but also the context in which appears. While doing code search it's important -to know where the function is defined, in which module, and in which file. This information is crucial to present the -results to the user in a meaningful way. +| Collection | Points | What it holds | +|---|---|---| +| `code-signatures-cloud` | 17,187 | function and struct signatures, textified into something close to English | +| `code-snippets-cloud` | 123,257 | the code itself, chunked on folding ranges from rust-analyzer | +| `code-files-cloud` | 1,720 | whole source files, no vectors, so results can be shown in context | -For example, the `upsert` function from one of Qdrant's modules would be represented as the following structure: +A search queries the first two at once. The signature collection supplies the +results; the snippet collection says which line ranges inside them the second +search also picked out, and those get highlighted. The file collection is a +filtered scroll, used when someone clicks into a result. + +### Chunking and Indexing + +Semantic search works best on structured source. Each chunk corresponds to a +function, struct, enum, or another unit that makes sense on its own. + +For the signature collection, the code is turned into a text-like representation +first, because a model trained on English does not read Rust. The `upsert` +function from `inverted_index_ram.rs` is stored as this structure: ```json { @@ -86,167 +110,381 @@ For example, the `upsert` function from one of Qdrant's modules would be represe "file_path": "lib/sparse/src/index/inverted_index/inverted_index_ram.rs", "file_name": "inverted_index_ram.rs", "struct_name": "InvertedIndexRam", - "snippet": " /// Upsert a vector into the inverted index.\n pub fn upsert(&mut self, id: PointOffsetType, vector: SparseVector) {\n for (dim_id, weight) in vector.indices.into_iter().zip(vector.values.into_iter()) {\n let dim_id = dim_id as usize;\n match self.postings.get_mut(dim_id) {\n Some(posting) => {\n // update existing posting list\n let posting_element = PostingElement::new(id, weight);\n posting.upsert(posting_element);\n }\n None => {\n // resize postings vector (fill gaps with empty posting lists)\n self.postings.resize_with(dim_id + 1, PostingList::default);\n // initialize new posting for dimension\n self.postings[dim_id] = PostingList::new_one(id, weight);\n }\n }\n }\n // given that there are no holes in the internal ids and that we are not deleting from the index\n // we can just use the id as a proxy the count\n self.vector_count = max(self.vector_count, id as usize);\n }\n" + "snippet": " /// Upsert a vector into the inverted index.\n pub fn upsert(&mut self, id: PointOffsetType, vector: SparseVector) { ... }" } } ``` -> Please note that this project aims to create a search mechanism specifically for Qdrant source code written in Rust. -Thus, we built a small separate [rust-parser project](https://github.com/qdrant/rust-parser) that converts it into the -before-mentioned JSON objects. It uses [Syn](https://docs.rs/syn/latest/syn/index.html) to read the syntax tree of the -codebase. If you want to replicate the project for a different programming language, you will need to build a similar -parser for that language. For example, Python has a similar library called [ast](https://docs.python.org/3/library/ast.html), -but there might be some differences in the way the code is parsed, thus some adjustments might be required. - -Since the `all-MiniLM-L6-v2` model is trained for more natural language tasks, it won't be able to understand the -code directly. For that reason, **we build a fake text-like representation of the structure, that should be -understandable for the model**, or its tokenizer to be more specific. Such representation won't contain the actual code, -but rather the important parts of it, like the function name, its signature, and the docstring, but also many more. All -the special, language-specific characters are removed, to keep the names and signatures as clean as possible. Only that -representation is then passed to the model. +and embedded as this sentence: -For example, the `upsert` function from the example above would be represented as: - -```python -'Function upsert that does: = " Upsert a vector into the inverted index." defined as fn upsert mut self id Point Offset Type vector Sparse Vector in struct InvertedIndexRam in module inverted_index in file inverted_index_ram.rs' -``` - -In the properly structured codebase, both module and file names should carry some additional information about the -semantics of that piece of code. For example, the `upsert` function is defined in the `InvertedIndexRam` struct, which -is a part of the `inverted_index`, which indicates that it is a part of the inverted index implementation stored in -memory. It is unclear from the function name itself. - -> If you want to see how the conversion is implemented in general, please check the `textify` function in the -`code_search.index.textifier` module. - -#### microsoft/unixcoder-base - -In that case, the model focuses specifically on the code snippets. We take the definitions along with the corresponding -docstrings and pass them to the model. Extracting all the definitions is not a trivial task, but there are various -Language Server Protocol (**LSP**) implementations that can help with that, and you should be able to [find one for -your programming language](https://microsoft.github.io/language-server-protocol/implementors/servers/). For Rust, we -used the [rust-analyzer](https://rust-analyzer.github.io/) that is capable of converting the codebase into the [LSIF -format](https://microsoft.github.io/language-server-protocol/specifications/lsif/0.4.0/specification/), which is a -universal, JSON-based format for code, regardless of the programming language. - -The same `upsert` function from the example above would be represented in LSIF as multiple entries and won't contain -the definition itself but just the location, so we have to extract it from the source file on our own. - -Even though the `microsoft/unixcoder-base` model does not officially support Rust, we found it to be working quite well -for the task. Obtaining the embeddings for the code snippets is quite straightforward, as we just send the code snippet -directly to the model: - -```rust -/// Upsert a vector into the inverted index. -pub fn upsert(&mut self, id: PointOffsetType, vector: SparseVector) { - for (dim_id, weight) in vector.indices.into_iter().zip(vector.values.into_iter()) { - let dim_id = dim_id as usize; - match self.postings.get_mut(dim_id) { - Some(posting) => { - // update existing posting list - let posting_element = PostingElement::new(id, weight); - posting.upsert(posting_element); - } - None => { - // resize postings vector (fill gaps with empty posting lists) - self.postings.resize_with(dim_id + 1, PostingList::default); - // initialize new posting for dimension - self.postings[dim_id] = PostingList::new_one(id, weight); - } - } - } - // given that there are no holes in the internal ids and that we are not deleting from the index - // we can just use the id as a proxy the count - self.vector_count = max(self.vector_count, id as usize); -} +```text +Function upsert that does: = " Upsert a vector into the inverted index." defined as +fn upsert mut self id Point Offset Type vector Sparse Vector in struct InvertedIndexRam +in module inverted_index in file inverted_index_ram.rs ``` -Having both encoders should help us build a more robust search mechanism, that can handle both the natural language and -code-specific queries. - -### Search process - -The search process is quite straightforward. The user input is passed to both encoders, and the resulting vectors are -used to query both Qdrant collections at the same time. The results are then merged with duplicates removed and returned -back to the user. - -## Architecture +Module and file names carry real information in a well-organised codebase. That +`upsert` belongs to `InvertedIndexRam` in `inverted_index` says it is the +in-memory inverted index, which the function name alone does not. The conversion +is `textify` in [`indexer/textifier.py`](indexer/textifier.py). -The demo uses [FastAPI](https://fastapi.tiangolo.com/) framework for the backend and [React](https://reactjs.org/) for -the frontend layer. +Extracting the structures needs a parser per language. Rust uses the +[rust-parser](https://github.com/qdrant/rust-parser) project, built on +[syn](https://docs.rs/syn/latest/syn/index.html). Snippet boundaries come from +rust-analyzer's [LSIF](https://microsoft.github.io/language-server-protocol/specifications/lsif/0.4.0/specification/) +output, which is language-agnostic. Any language with an +[LSP implementation](https://microsoft.github.io/language-server-protocol/implementors/servers/) +can be chunked the same way. -![Architecture of the code search demo](images/architecture-diagram.png) +### Search -The demo consists of the following components: -- [React frontend](/frontend) - a web application that allows the user to search over Qdrant codebase -- [FastAPI backend](/code_search/service.py) - a backend that communicates with Qdrant and exposes a REST API -- [Qdrant](https://qdrant.tech/) - a vector search engine that stores the data and performs the search -- Two neural encoders - one trained on the natural language and one for the code-specific tasks +Both collections are hybrid: a dense vector and a sparse one per point, both +produced by models running inside the cluster. -The diagram shows two collections. There are three: `code-signatures` for the -MiniLM vectors, `code-snippets-unixcoder` for the UniXcoder vectors, and -`code-files` holding the source of every indexed file so results can be shown in -context. - -There is also an additional indexing component that has to be run periodically to keep the index up to date. It is also -part of the demo, but it is not directly exposed to the user. All the required scripts are documented below, and you can -find them in the [`tools`](/tools) directory. - -The demo is, as always, open source, so feel free to check the code in this repository to see how it is implemented. - -## Usage - -As every other semantic search system, the demo requires a few steps to be set up. First of all, the data has to be -ingested, so we can then use the created index for our queries. - -### Data indexing - -Qdrant is used as a search engine, so you will need to have it running somewhere. You can either use the local container -or the Cloud version. If you want to use the local version, you can start it with the following command: - -```shell -docker run -p 6333:6333 -p 6334:6334 \ - -v $(pwd)/qdrant_storage:/qdrant/storage:z \ - qdrant/qdrant +```jsonc +{ + "prefetch": [ + { "query": { "text": "cardinality of should request", + "model": "sentence-transformers/all-MiniLM-L6-v2" }, + "using": "dense", "limit": 100 }, + { "query": { "text": "cardinality of should request", + "model": "Qdrant/bm25" }, + "using": "sparse", "limit": 100 } + ], + "query": { "fusion": "rrf" }, + "limit": 5 +} ``` -However, the easiest way to start using Qdrant is to use our Cloud version. You can sign up for a free tier 1GB cluster -at [https://cloud.qdrant.io/](https://cloud.qdrant.io/). - -Once the environment is set up, you can configure the Qdrant instance and build the index by running the following -commands: +One round trip. The cluster embeds the query with both models, runs both +searches, fuses them with +[reciprocal rank fusion](https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf) +(Cormack, Clarke and Buettcher, 2009), and returns five results. The client +sends a string. + +Nothing here is a new technique. Dense and sparse retrieval fused with RRF is a +standard hybrid setup, and Qdrant implements the fusion. What this repository +contributes is the measurement: which of the ten configurations actually wins on +code, and by how much. + +Why both legs: dense retrieval and BM25 fail on opposite queries, and the +benchmark below shows exactly where. Someone searching `estimate_cardinality` +wants the lexical match. Someone searching "how does it guess how many points +come back" needs the dense one. Fusing costs one extra prefetch and no extra +round trip. + +BM25 is worth a separate note: Qdrant computes it in-engine from collection +statistics, so unlike every other model in the catalog it bills **no inference +tokens**. The sparse half of every query here is free. + +## Benchmarks + +Every number here came out of [`bench/`](bench) and can be re-run. Nothing is +quoted from a model card. + +### Picking the Encoder + +`bench/bakeoff.py` indexes 5,000 chunks of `qdrant/qdrant` into one collection +with four vectors per point, then scores ten retrieval configurations through +the real production path: the cluster embeds the query, the cluster searches, +the cluster fuses. + +Two query sets, because they disagree, and the disagreement is the finding: + +- **docstring**: 300 docstrings lifted from the code they describe, so they + share identifiers with the answer. +- **paraphrase**: 113 of the same intents rewritten to avoid those + identifiers. Closer to what a person types. + +| configuration | doc R@1 | doc R@10 | doc MRR | par R@1 | par R@10 | par MRR | p50 ms | +|---|---|---|---|---|---|---|---| +| minilm only | 0.613 | 0.863 | 0.694 | 0.071 | 0.327 | 0.137 | 53 | +| mxbai only | 0.817 | 0.973 | 0.876 | 0.106 | 0.381 | 0.175 | 112 | +| bm25 only | 0.893 | 0.993 | 0.932 | 0.035 | 0.142 | 0.068 | 32 | +| splade only | 0.903 | 0.993 | 0.935 | 0.035 | 0.133 | 0.068 | 110 | +| minilm + bm25 (RRF) | 0.787 | 0.983 | 0.870 | 0.071 | 0.283 | 0.134 | 56 | +| minilm + splade (RRF) | 0.787 | 0.983 | 0.870 | 0.088 | 0.292 | 0.144 | 113 | +| mxbai + bm25 (RRF) | 0.877 | 0.993 | 0.924 | 0.133 | 0.336 | 0.178 | 116 | +| mxbai + splade (RRF) | 0.877 | 1.000 | 0.925 | 0.124 | 0.336 | 0.171 | 118 | +| minilm + bm25 (DBSF) | 0.843 | 0.980 | 0.896 | 0.097 | 0.239 | 0.140 | 56 | +| mxbai + bm25 (DBSF) | 0.897 | 0.990 | 0.932 | 0.133 | 0.292 | 0.167 | 119 | + +5,000 candidates, one correct answer each, 100 candidates per leg before +fusion. p50 is the Qdrant round trip from a laptop in the same country as the +cluster, over a kept-alive connection. + +What it says: + +**Lexical search wins the docstring set and loses the paraphrase set, badly.** +BM25 alone gets 0.993 recall@10 when the query shares identifiers with the +answer, and 0.142 when it does not. Any benchmark built only from docstrings +would conclude that the dense leg is dead weight. It would be wrong about every +query a visitor actually types. + +**SPLADE never earns its cost.** It ties BM25 on the docstring set, loses on +the paraphrase set, runs three times slower, and bills inference tokens where +BM25 bills none. BM25 is computed in-engine from collection statistics, so the +sparse half of this system is free at any corpus size. + +**DBSF was tried and rejected.** Reciprocal rank fusion gives each leg half a +vote by position, which looked wasteful on paraphrase queries where the lexical +leg contributes noise. Distribution-based fusion should have let a confident +dense score outvote a weak lexical one. It did the opposite: it lets a +confident *lexical* score dominate, and on paraphrases BM25 is confidently +wrong. Paraphrase recall@10 fell from 0.336 to 0.292. + +### A Model Served Differently Than Its Model Card + +The in-cluster `all-MiniLM-L6-v2` scores 0.863 docstring recall@10 here. An +offline run of the same model, on the same 5,000 documents and the same 300 +queries, scored 0.933. + +Probing the service with progressively longer input shows it truncates at +**512 tokens**. The published `sentence-transformers` configuration for this +model sets `max_seq_length` to **256**. It was trained at that length, and +positions beyond it are ones it barely saw. 36% of this corpus is longer than +256 tokens, so more than a third of it was being embedded with positions the +model was never trained on. + +`bench/truncation.py` indexes the corpus again with every document cut to 700 +characters, roughly 256 tokens of Rust, and scores it the same way. + +| input | doc R@10 | doc MRR | par R@10 | par MRR | +|---|---|---|---|---| +| minilm, full text | 0.863 | 0.694 | 0.327 | 0.137 | +| minilm, cut to ~256 tokens | 0.937 | 0.791 | 0.310 | 0.167 | +| minilm + bm25, full text | 0.983 | 0.870 | 0.283 | 0.134 | +| minilm + bm25, cut | 0.983 | 0.913 | 0.319 | 0.126 | + +Cutting the input takes MiniLM from 0.863 to 0.937 docstring recall@10, which is +the 0.933 the offline run got. The window is the cause. + +Two things follow. Anyone using `all-MiniLM-L6-v2` through Qdrant Cloud Inference on +documents longer than 256 tokens is losing recall and has no way to see it from +the API. And for this demo it is moot, because `mxbai-embed-large-v1` has a +native 512-token limit, so the service's window is the model's window and there +is nothing to cut. [`indexer/build.py`](indexer/build.py) still carries an +`INDEX_CHAR_BUDGET` for anyone who switches back to a 256-token model; it +defaults to 0. + +### Why mxbai and Not the Free Model + +| | mxbai + bm25 | minilm + bm25 (cut) | +|---|---|---| +| docstring recall@10 | **0.993** | 0.983 | +| docstring MRR | **0.924** | 0.913 | +| paraphrase recall@10 | **0.336** | 0.319 | +| paraphrase MRR | **0.178** | 0.126 | +| paraphrase recall@1 | **0.133** | 0.035 | +| p50 | 116 ms | **54 ms** | +| dimensions | 1024 | **384** | +| tokens to index 140k points | 65.4 M | **0**, MiniLM is free | + +On docstring queries these are close enough that either would do. They separate +on the paraphrase set, and that is the set that matters: a visitor types a +description, not a docstring. mxbai puts the right function first for 13.3% of +those queries against MiniLM's 3.5%. The demo is judged on the first result, so +it pays for the model. + +Reproduce the whole comparison with `python bench/bakeoff.py` and +`python bench/truncation.py`. + +### What a Full Index Costs + +The index that is deployed was built in one run, and the cluster reports what it +charged, so these are measured rather than projected. + +| collection | points | wall clock | mxbai tokens | +|---|---|---|---| +| `code-files-cloud` | 1,720 | 4 s | 0, it holds no vectors | +| `code-signatures-cloud` | 17,187 | 3.4 min | 2,262,210 | +| `code-snippets-cloud` | 123,257 | 22.3 min | 28,164,640 | +| **total** | **142,164** | **~26 min** | **30,426,850** | + +BM25 appears nowhere in that column. The engine derives it from collection +statistics, so the sparse half of the index cost nothing and will keep costing +nothing however far the corpus grows. + +`bench/index_cost.py` estimates this ahead of a run by embedding a uniform +sample and multiplying. It predicted 65.4M against the 30.4M actually charged, +and the gap is instructive: it sampled the evaluation corpus, whose text field +is a signature and its snippet concatenated, while the deployed collections +embed each of those separately. Sample the thing you are going to embed, not +something shaped like it. + +Per-model prices live on the Inference tab of the cluster page in the Cloud +Console and are deliberately not copied here, because a stale price in a +repository is worse than no price. A full rebuild is a one-off in any case. +Answering a search costs 36 to 46 tokens, measured on the deployed demo: the +query is embedded once per collection, and the two BM25 legs cost nothing. The +`/api/search` response carries the figure as `inference_tokens` so it stays +checkable rather than asserted. + +### Against the Demo It Replaces + +Both deployments answered the same 413 queries over the same 123k corpus on the +same day, five results each, scored by `bench/quality.py`. + +| | R@1 | R@5 | file R@5 | MRR | +|---|---|---|---|---| +| **rebuild**, docstring | **0.887** | **0.983** | **0.983** | **0.930** | +| old demo, docstring | 0.567 | 0.907 | 0.913 | 0.713 | +| **rebuild**, paraphrase | 0.097 | 0.177 | 0.257 | 0.127 | +| old demo, paraphrase | 0.106 | 0.195 | 0.257 | 0.142 | + +The paraphrase row looks like a small regression and is not one. +`bench/significance.py` compares the two rankings query by query with a paired +bootstrap and an exact sign test, because a 0.015 gap across 113 queries is well +inside what four queries landing differently can produce: + +| comparison | MRR difference | 95% interval | sign test | +|---|---|---|---| +| docstring | **+0.125** | [+0.091, +0.161] | p < 0.001, 66 better / 11 worse | +| paraphrase | −0.012 | [−0.074, +0.051] | p = 0.44, 11 better / 16 worse | + +The docstring gain is real and large. The paraphrase difference cannot be +distinguished from zero on this many queries, so the honest claim is a decisive +win on one set and a draw on the other, not a trade. + +The sparse leg was checked the same way rather than assumed. Against dense +alone it is worth +0.041 MRR on docstring queries, interval [+0.020, +0.062], +p = 0.001. On paraphrase queries dense alone wins more queries than it loses, +16 to 3, but the MRR interval straddles zero. So BM25 earns its place on the +evidence that exists, and the case against it does not. + +### Where This Stops Working + +Four boundaries, all of them visible in the numbers above. + +**Vague natural language still mostly fails.** Paraphrase recall@5 is 0.177. A +visitor who types "how does it decide what to keep in memory" will usually not +get the right function in five results, from this build or the one it replaces. +The docstring numbers are the flattering ones and they are flattering because +those queries share identifiers with their answers. + +**One repository, one language.** Chunking depends on rust-analyzer for snippet +boundaries and on `qdrant/rust-parser` for signatures. Another language needs +its own parser and an LSP that emits LSIF. Nothing above transfers untested. + +**142k points on one cluster.** Everything here was measured at that size. RRF +behavior and the prefetch of 100 per leg were not tuned for a corpus an order +of magnitude larger, and the sparse leg's advantage is a property of this +corpus's identifier vocabulary rather than a general result. + +**The index is a snapshot.** Results resolve against one commit of +`qdrant/qdrant`. The moment that branch moves, line numbers drift, and nothing +in the running demo notices. `INDEXED_COMMIT` is the seam. + +### A Ranking Bug, Inherited + +The first head-to-head scored the rebuild at 0.620 docstring recall@1 while the +signature search underneath it was returning the right function first 88.7% of +the time. The merge step was throwing that away. + +`merge_search_results` sorted results by how many overlapping snippet ranges +each one had, on the theory that results both searches agreed on should come +first. In practice a result with two highlighted line ranges was promoted over +the actual best match with none. Removing the re-sort moved docstring recall@1 +from 0.620 to 0.887 and MRR from 0.768 to 0.930, and changed nothing else: +overlap still decides which lines are highlighted. + +This was inherited, not introduced. Measured directly, the old demo's own +signature search scores 0.730 recall@1, while the deployment built on top of it +returns 0.567. Same re-sort, same cost, and it has been there the whole time. + +### Latency + +Both deployments, 60 searches each, from the same machine over a kept-alive +connection, queries varied so nothing is answered from a cache. + +| | first request | p50 | p95 | server p50 | +|---|---|---|---|---| +| **rebuild** | 495 ms | **148.8 ms** | 233.2 ms | 91 ms | +| old demo | 438 ms | 176.7 ms | **188.2 ms** | **81 ms** | + +`server` is what the function reports it spent talking to Qdrant, so the gap +between it and end-to-end is the platform and the network. + +The rebuild is faster at the median and slower at the p95. That tail is the +honest cost of serverless: a container that is always warm has a flatter +distribution than functions that are not. The first request is comparable, which +surprises people who expect a cold start to dominate. There is no model to load +any more, so there is nothing to be cold about. + +**Where the tail comes from.** Measured directly against Qdrant, the BM25 leg +answers in 32.0 ms p50 and 33.4 ms p95, a spread of 1.4 ms. The mxbai leg +answers in 110.9 ms p50 and 156.9 ms p95, a spread of 46.0 ms. The function's +whole server-side spread is 54 ms, so 46 of it is the dense model's inference +call and the platform contributes about eight. The old demo ran a smaller model +in-process on a warm container, which is why its distribution is nearly flat at +80 ms p50 and 84 ms p95. That determinism is what was traded for a model that +finds the right function 56% more often. + +**Repeated queries are free.** The three example queries on the landing page get +clicked far more than anything anyone types, and Qdrant Cloud Inference re-embeds +every request: an identical query costs the same 117 ms as a novel one. So the +function keeps a small bounded cache of whole responses, which skips the +embedding and the search together. On traffic where the examples dominate, p50 +falls to 52.9 ms, most of which is the network rather than the demo. On the +benchmark above, where every query is deliberately different, it changes nothing. + +The old backend cached the query vector instead. That option is gone: the +cluster embeds and never returns the vector, so there is nothing of that shape +to keep. Caching the answer is strictly more of the work, and it is only safe +because the index is a snapshot rebuilt by an explicit run rather than updated +underneath the reader. + +**Region pinning is doing most of the work here.** The cluster is in AWS +us-west-2 and Vercel defaults functions to iad1 in Virginia, so every search +crossed the country and back, twice, once per collection. Before pinning to +pdx1, the same benchmark reported 273.8 ms p50 and 163 ms server. One line in +[vercel.json](vercel.json) took the median from 273.8 ms to 148.8 ms. Anyone +deploying this against a cluster in another region should change that line +first, and the health endpoint will not tell them: a cross-region deployment is +perfectly healthy and quietly twice as slow. + +## Running It Yourself + +See [DEPLOY.md](DEPLOY.md). Short version: create a Qdrant Cloud cluster with +inference enabled, build the index, import this repo into Vercel, set +`QDRANT_URL` and `QDRANT_API_KEY`. + +### Prerequisites for Building the Index From Source + +- **Qdrant 1.10 or newer**, with Qdrant Cloud Inference enabled. The two-leg + `prefetch` plus `fusion` query needs 1.10; inference is what embeds the query. +- [Rust](https://www.rust-lang.org/learn/get-started) and + [rust-analyzer](https://rust-analyzer.github.io/) for snippet boundaries +- [Docker](https://www.docker.com/) for the rust-parser container ```shell -export QDRANT_URL="http://localhost:6333" - -# For the Cloud service you need to specify the api key as well -# export QDRANT_API_KEY="your-api-key" - +rustup component add rust-analyzer +export QDRANT_URL="https://your-cluster-id.region.aws.cloud.qdrant.io:6333" +export QDRANT_API_KEY="..." bash tools/download_and_index.sh ``` -The indexing process might take a while, as it needs to encode all the code snippets and send them to the Qdrant. - -### Search service +Nothing in that list is a Python model dependency, and there is no +`requirements.txt`. The indexer is stdlib. -Once the index is built, you can start the search service by running the following commands: +### Local Development ```shell -docker-compose up +cp .env.example .env.local # QDRANT_URL and QDRANT_API_KEY +npm install +npx vercel dev ``` -The UI will be available at [http://localhost:8000/](http://localhost:8000/), serving both the -frontend and the API from the same container. For a running instance, see the -[live demo](https://demo-code-search-production.up.railway.app). - -You can type in the search query and see the related code structures. Queries might come both from natural language -but also from the code itself. - -## Further steps +http://127.0.0.1:3000 serves the app and the API together, the same way +production does. -If you would like to take the demo further, you can try to: +## Further Steps -1. Disable one of the neural encoders and see how the search results change. -2. Try out some other encoder models and see the impact on the search quality. -3. Fork the project and support programming languages other than Rust. -4. Build a ground truth dataset and evaluate the search quality. +1. Turn off one leg of the hybrid query and watch which queries break. The + paraphrase set below is the interesting half. +2. Swap the dense model. `bench/bakeoff.py` scores a new one against the same + corpus in one command. +3. Fork it for a language other than Rust. The chunking is the only part that + is Rust-specific. +4. Build a ground truth set for your own codebase and re-run `bench/quality.py` + against it. Every claim on this page came out of that script. diff --git a/REVIEW-RESPONSE.md b/REVIEW-RESPONSE.md new file mode 100644 index 0000000..1d33545 --- /dev/null +++ b/REVIEW-RESPONSE.md @@ -0,0 +1,155 @@ +# Response to the Previous Review + +Every item kanungle raised on +[qdrant/demo-food-discovery#31](https://github.com/qdrant/demo-food-discovery/pull/31) +and [qdrant/demo-code-search#32](https://github.com/qdrant/demo-code-search/pull/32), +checked against this repository. His closing note on #31 was to run the +`neil-review` skill before submitting, which was done, and its own findings are +at the bottom. + +Two of his items were open here when this audit started. Both are fixed. + +## demo-food-discovery#31: The Eight Must-Fix Items + +### 1. The Demo Names the Wrong Qdrant API + +His finding: the hero said Discovery API while the code called +`recommend_groups()`. "This is our own demo teaching developers the wrong API +name." + +**Here:** the product is **Qdrant Cloud Inference**, and the search is the +**Query API** (`points/query` with `prefetch` and `fusion`). Bare "Cloud +Inference" was corrected to the full product name in 15 places. The badge +tooltip names the two models from the API response rather than from a hardcoded +string, so it cannot drift from what actually answered. + +### 2. False Claims in the "How It Works" Modal + +His finding: four claims the code did not support, plus one that was only true +under a non-default strategy. + +**Here:** the encoder swap made three claims false, and all three are corrected. + +| was | now | why it was wrong | +|---|---|---| +| "Search Code by Meaning, **Not Keywords**" | "Search Code by Meaning, **and by Name**" | the demo runs BM25; keyword matching answers 0.980 recall@5 on the docstring set | +| "This demo runs **semantic** search" | "This demo runs **hybrid** search" | dense plus sparse, fused | +| badge: **Semantic** | badge: **Hybrid** | same claim, shown next to every result | +| "MiniLM reads the description, UniXcoder reads the code" | "mxbai reads the description, BM25 reads the identifiers" | neither model is in the build any more | + +The two stat claims were checked against the live collection rather than carried +over: 14,604 functions (`code_type == "Function"`, counted) and 1,720 files. +Both hold. + +### 3. The Description Says the Backend Is Unchanged When It Changed + +His finding: reviewers skip a diff the description tells them to skip. + +**Here:** this was open. An earlier draft of the PR description said the merge +step was "output identical" to the original. It is not, deliberately: the +re-sort is gone. The description now leads with four behavioural changes before +any of the hosting story. + +### 4. The Declared Dependency Floor Cannot Run the New Code + +His finding: `qdrant-client = "^1.6.0"` while the code needed 1.11, which +resolves fine for the author and breaks for anyone pinned. + +**Here:** this was open, and it is the same class of gap. There are no Python or +client dependencies left to pin, but the *server* has a floor and nothing +declared it. The search is a two-leg `prefetch` with a `fusion` step, which the +Query API gained in **Qdrant 1.10**. Now stated in both the README prerequisites +and DEPLOY.md, with the note that Qdrant Cloud Inference is a separate +requirement: a 1.19 cluster with inference switched off still cannot embed a +query. + +### 5. Deleting `.gitignore` Leaves `node_modules` Trackable + +**Here:** root `.gitignore` covers `node_modules/`, `__pycache__/`, `*.pyc`, +`.env`, `.env.local`, `.vercel`, `.test-build/`, `data/` (with `!data/.keep` so +the directory the indexing scripts write into survives a fresh clone), +`indexer/.state/`, and `bench/results/*.log`. `frontend/.gitignore` is still +present. + +### 6. The `vercel.json` Catch-All Rewrite Swallows the API + +**Here:** the rewrite is `/((?!api/)(?!.*\.).*)`, which excludes the API prefix +and anything with a file extension. Verified against the deployment rather than +by reading it: `/api/search`, `/api/file` and `/api/health` all answer 200 in +production. + +### 7. Search Failures Are Silent + +His finding: `.error-state` was fully styled and never rendered, so a dead +backend looked like a working demo showing stale cards. He tested it by forcing +the API to fail. + +**Here:** tested the same way. Pointing `QDRANT_NLU_COLLECTION` at a collection +that does not exist returns +`503 Search index is unavailable: a Qdrant collection is missing`, and the UI +renders the "Something Went Wrong" banner with the previous results cleared. The +API distinguishes a missing collection (503) from a query with no matches (200 +and an empty list), which the old backend did not: it answered 200 either way, +and that is how the demo sat broken without anyone noticing. + +### 8. The README No Longer Describes This App + +His finding: the README credited a UI kit the PR deleted, and the screenshots +showed the old interface. + +**Here:** rewritten around what the code now does. No stale screenshots exist, +because there are none: the only image is `images/architecture.svg`, redrawn for +this build and checked against the running system (two vendors, the model names, +the three collection sizes). The old `architecture-diagram.png` showed FastAPI +and Railway and was deleted rather than kept. + +## demo-code-search#32: The Three Questions + +**"Why are you gating the QDRANT_API_KEY to JWTs only?"** Not gated. The app +checks that a key is present when the URL is remote and nothing else. The only +JWT-shaped check left is a `note:` line in the indexing workflow, which logs and +carries on, because self-hosted keys are not JWTs. + +**"Are payload indexes properly created with file_uploader.py?"** Yes, and +verified live rather than assumed. `indexer/build.py` creates a keyword index on +`path` with `wait=true`; the deployed collection reports +`{'path': {'data_type': 'keyword', 'points': 1720}}`. Without it, clusters with +strict mode on refuse to filter an unindexed field and the file viewer 500s on +every result while search keeps working, which reads like a frontend bug. + +**"Has the README been updated enough?"** Rewritten, not patched. Every number +in it comes from a script in `bench/` that can be re-run, and the sections that +would have been the weakest, what this gives up and where it stops working, are +both there. + +## neil-review Findings on This Repository + +Run before submitting, as he asked. Fixed: Title Case on 18 headings, "vector +database" removed, US English, reciprocal rank fusion now credits Cormack et al. +2009 with the contribution stated as the measurement rather than the method, and +a "Where This Stops Working" section naming four boundaries. + +The checker also reports 13 missing frontmatter fields and an absolute +`qdrant.tech` link. Those are `qdrant/landing_page` article rules and do not +apply to a repository README, so they are left alone. + +## What Is Deliberately Different + +The goal was a better demo on two vendors, not a byte-identical one. Four things +behave differently and each is either forced or measured: + +1. **The encoder.** Forced. UniXcoder is not in the Qdrant Cloud Inference + catalog and cannot be added. +2. **Result ordering.** Chosen. Removing the highlight-count re-sort moved + docstring recall@1 from 0.620 to 0.887. +3. **The "Warming Up" fallback.** Gone with the backend that produced it, and it + never fired in production: it needed a `data/structures.json` that was never + in the deployed image. Confirmed against the live old demo, which reports no + `mode` field on any query. +4. **Response caching.** The old build cached the query vector. Qdrant Cloud + Inference never returns the vector, so the response is cached instead. + +Everything else was held constant on purpose, and checked: the CSS file is +byte-identical, the per-result JSON keys are identical, the corpus is the same +1,720 files, and `indexed_commit` is the same `74f3e85b`, so every result link +resolves to the same code. diff --git a/api/_lib/cache.ts b/api/_lib/cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..a31caf6b9fff2e13fd7f0b48945c98712a4289a8 GIT binary patch literal 2485 zcma)8!A>Jb5X~8hf2hMsu`L)!k&**hSRsP4C|aa|54l0Rr|fBY+&$afW8)?KdtOzK zG1;u5a50|ge)X#A)hkaYlj9?r&?RN(cQg;Zuel~`Y{Rq&krJC|8;YE|;1j14YPhY* zdCGF4_cpuW@jYeZEmwl3h%>wnI5{_V#eRUS_15Q}dPLMeLT7?vz?Uu%*m<>+i~ceDkLk~dvyLO#=YHnc68fkxsTE=8XAuFW*xfs)eTr>=Dt zg;xyW8hcM>X(S!sboYMM*1fnG3hVwuUsNer8}_C0&dRi9eZj zhw8|62XaYzQ6{xTZKQ3TSr(;AtspH7fWv^+v@o#<3hAMdMt7_d0PWTNJ&V!x^GuP^VeK0aJsUVXfNJ^A4S{rJ<* z<6i_vVCh;q{C;);jAz=Iw&ydN(Tg4^ff9Uz@!Ip2KAG;lOn_gW3^#kf@?q;QE=C0u zS`9HDyh%OfcixS(s%JVsa=UfV>KS?EJ>Ea3XOtVA$bIzm`0iOxy?poXS#Q5Qg4T;r z|7MimI3tnKig$ZB;LhkTmC3@%h$wtXt3i9tK?liIXoXyp42axi5WlR_WS9q0vWhhAWrHC##jLWfI@40BaiQcPGWm$8$`Kr2vU>LEhQ zZ;WRva$K9$FjBmM4avDVw_!^yo0L!YQBNi6fEY0yGD9k?ICoHO+=-Zm+-q(*^CA6G zkyI*zh2wH5%g_{juT&;v63zazANT+2$H}Y0(3wqeLqnwEC2V7oG8z9ra)V;&rBEz> zHb5o!h3Vq$n2byN305Klg#F0)*lPeiN~3Y*`6;WE34cPY9II*!;+W>Jn3Z<^tMUz} zv@(SZbgELF4ohAJO>DvT5i3!op}QO|KM|*!yc*NNzi#1$a3#zWG=zgw)46HGrp?V6 zF?QTx%wUz+z=TPB#VM~D~3!8;(q?2Bs-`&c%90p@j=Wf|bt ziNcAAZS^NYAxS^Tj~2Nm=7|-ElA*x52gJj}T%``HmGJ^&@zO`ds~;6{(HiXa;+bFl z_8ER)><0q74z*mwuvQdVgktI<^IXW;-0PAno1g2we}2DG+FHEA1*Y^0}d3i F{|)8rKhOXG literal 0 HcmV?d00001 diff --git a/api/_lib/config.ts b/api/_lib/config.ts new file mode 100644 index 0000000..8afd417 --- /dev/null +++ b/api/_lib/config.ts @@ -0,0 +1,42 @@ +/** + * One place for the names of things. + * + * Every model here is served from inside the Qdrant cluster. External providers + * (openai/, cohere/, jinaai/, openrouter/) are reachable through Cloud + * Inference too, but each one is a second vendor with a second API key, which is + * the thing this rebuild exists to remove. If you change a model, change it to + * another in-cluster one. + */ + +export const CODE_COLLECTION = process.env.QDRANT_CODE_COLLECTION ?? "code-snippets-cloud"; +export const NLU_COLLECTION = process.env.QDRANT_NLU_COLLECTION ?? "code-signatures-cloud"; +export const FILE_COLLECTION = process.env.QDRANT_FILE_COLLECTION ?? "code-files-cloud"; + +/** + * Dense encoder. Chosen by bench/bakeoff.py; see the table in the README. + * + * mxbai beat all-MiniLM-L6-v2 on both query sets and beat it decisively on the + * one that matters for a demo: 13.3% of natural-language paraphrase queries put + * the right function first, against 3.5% for MiniLM. + */ +export const DENSE_MODEL = + process.env.QDRANT_DENSE_MODEL ?? "mixedbread-ai/mxbai-embed-large-v1"; +export const DENSE_VECTOR = "dense"; + +/** + * Sparse encoder. BM25 is computed by the engine from collection statistics, so + * unlike every other model in the catalog it bills no inference tokens - the + * sparse leg of a hybrid query is free. + */ +export const SPARSE_MODEL = process.env.QDRANT_SPARSE_MODEL ?? "Qdrant/bm25"; +export const SPARSE_VECTOR = "sparse"; + +/** Candidates each leg contributes to the fusion before the final cut. */ +export const PREFETCH_LIMIT = Number(process.env.PREFETCH_LIMIT ?? 100); + +/** + * Commit of qdrant/qdrant the collections were built from. Result links carry + * line numbers, and resolving them against a moving `master` quietly points at + * the wrong lines as the source changes. + */ +export const INDEXED_COMMIT = process.env.INDEXED_COMMIT ?? "master"; diff --git a/api/_lib/merge.ts b/api/_lib/merge.ts new file mode 100644 index 0000000..9d4affe --- /dev/null +++ b/api/_lib/merge.ts @@ -0,0 +1,92 @@ +/** + * Merge the two searches into the shape the UI renders. + * + * A port of the old backend's postprocessing.merge_search_results, with one + * deliberate difference: it no longer re-ranks. See the note on the sort at the + * bottom of this file for the measurement behind that. + * + * The signature search supplies the results. The snippet search only says which + * line ranges inside those results the second model also picked out, which the + * UI highlights. A result with no overlap still shows, it just shows unhighlighted. + */ + +export type CodeHit = { + file: string; + start_line: number; + end_line: number; +}; + +export type SubMatch = { + overlap_from: number; + overlap_to: number; +}; + +export type NluHit = { + code_type: string; + context: { + file_name: string; + file_path: string; + module: string; + snippet: string; + struct_name: string | null; + }; + docstring: string | null; + line: number; + line_from: number; + line_to: number; + name: string; + signature: string; + sub_matches?: SubMatch[]; +}; + +/** Line ranges where a snippet hit and a signature hit cover the same lines. */ +export function overlappingSnippets(codeHits: CodeHit[], nluHit: NluHit): SubMatch[] { + const overlapped: SubMatch[] = []; + const sorted = [...codeHits].sort((a, b) => a.start_line - b.start_line); + + for (const hit of sorted) { + // The snippet collection stores 0-based lines and the signature collection + // stores 1-based ones. The +1 reconciles them; dropping it shifts every + // highlight up by a line, which is subtle enough to survive review. + const fromA = hit.start_line + 1; + const toA = hit.end_line + 1; + const start = Math.max(fromA, nluHit.line_from); + const end = Math.min(toA, nluHit.line_to); + if (start <= end) { + overlapped.push({ overlap_from: start, overlap_to: end }); + } + } + + return overlapped; +} + +export function mergeSearchResults(codeHits: CodeHit[], nluHits: NluHit[]): NluHit[] { + const byFile = new Map(); + for (const hit of codeHits) { + const existing = byFile.get(hit.file); + if (existing) existing.push(hit); + else byFile.set(hit.file, [hit]); + } + + for (const nluHit of nluHits) { + const forFile = byFile.get(nluHit.context.file_path); + if (forFile) { + nluHit.sub_matches = overlappingSnippets(forFile, nluHit); + } + } + + // The search's ranking is returned as-is. + // + // The original sorted by how many highlight ranges each result had, on the + // theory that results both searches agreed on should come first. Measured + // against 300 docstring queries over the full corpus, that re-sort cost + // 0.273 recall@1 and 0.165 MRR: the signature search puts the right function + // first 89.3% of the time, and re-ordering by highlight count dropped that to + // 62.0%. A result with two highlighted ranges was being promoted over the + // actual best match with none. + // + // Overlap is a weaker signal than the ranking it was overriding, so it stays + // where it belongs, deciding which lines to highlight and nothing else. + // bench/fusion_check.py reproduces both numbers. + return nluHits; +} diff --git a/api/_lib/qdrant.ts b/api/_lib/qdrant.ts new file mode 100644 index 0000000..6496932 --- /dev/null +++ b/api/_lib/qdrant.ts @@ -0,0 +1,88 @@ +/** + * Minimal Qdrant REST client. + * + * The old backend carried torch, transformers and sentence-transformers so it + * could turn a query into a vector before asking Qdrant anything. Cloud + * Inference does that inside the cluster, so the client's entire job is to post + * JSON. That is small enough that a dependency would be the larger half of it, + * and keeping it dependency-free is what lets these functions cold-start in + * milliseconds instead of loading a model. + */ + +const RAW_URL = process.env.QDRANT_URL ?? ""; +const API_KEY = process.env.QDRANT_API_KEY ?? ""; + +if (!RAW_URL) { + throw new Error("QDRANT_URL is not set"); +} +if (RAW_URL.startsWith("https://") && !API_KEY) { + // Fail loudly at module load rather than returning 500s per request. A + // missing variable on a remote cluster is a deploy problem, and surfacing it + // as "unauthorized" on every search sends whoever debugs it the wrong way. + throw new Error(`QDRANT_URL is remote (${RAW_URL}) but QDRANT_API_KEY is not set`); +} + +const BASE = RAW_URL.replace(/\/+$/, ""); + +/** Milliseconds before a Qdrant call is abandoned. */ +const TIMEOUT_MS = Number(process.env.QDRANT_TIMEOUT_MS ?? 15000); + +export type InferenceUsage = { + inference?: { models?: Record }; +}; + +export class QdrantError extends Error { + constructor( + message: string, + readonly status: number, + ) { + super(message); + this.name = "QdrantError"; + } +} + +export async function post(path: string, body: unknown): Promise<{ result: T; usage?: InferenceUsage }> { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); + let response: Response; + try { + response = await fetch(`${BASE}${path}`, { + method: "POST", + headers: { "api-key": API_KEY, "content-type": "application/json" }, + body: JSON.stringify(body), + signal: controller.signal, + }); + } catch (err) { + const aborted = err instanceof Error && err.name === "AbortError"; + throw new QdrantError( + aborted ? `Qdrant did not answer within ${TIMEOUT_MS}ms` : String(err), + aborted ? 504 : 502, + ); + } finally { + clearTimeout(timer); + } + + const text = await response.text(); + let payload: { result?: T; status?: unknown; usage?: InferenceUsage }; + try { + payload = JSON.parse(text); + } catch { + throw new QdrantError(`Qdrant returned non-JSON (${response.status}): ${text.slice(0, 200)}`, 502); + } + + if (!response.ok || payload.result === undefined) { + const detail = + typeof payload.status === "object" && payload.status !== null && "error" in payload.status + ? String((payload.status as { error: unknown }).error) + : text.slice(0, 300); + throw new QdrantError(detail, response.status === 404 ? 404 : 502); + } + + return { result: payload.result, usage: payload.usage }; +} + +/** Total billable inference tokens across every model in one response. */ +export function tokensUsed(usage?: InferenceUsage): number { + const models = usage?.inference?.models ?? {}; + return Object.values(models).reduce((sum, m) => sum + (m.tokens ?? 0), 0); +} diff --git a/api/_lib/search.ts b/api/_lib/search.ts new file mode 100644 index 0000000..658f683 --- /dev/null +++ b/api/_lib/search.ts @@ -0,0 +1,76 @@ +import { + CODE_COLLECTION, + DENSE_MODEL, + DENSE_VECTOR, + NLU_COLLECTION, + PREFETCH_LIMIT, + SPARSE_MODEL, + SPARSE_VECTOR, +} from "./config.js"; +import { mergeSearchResults, type CodeHit, type NluHit } from "./merge.js"; +import { post, tokensUsed } from "./qdrant.js"; + +type Scored = { id: string | number; score: number; payload: T }; + +/** + * A hybrid query: dense and sparse legs, fused server-side with reciprocal rank + * fusion. + * + * Both legs carry the query as text rather than as a vector. The cluster embeds + * it, searches, and fuses in one round trip, which is why this repository has no + * model runtime in it at all. The dense leg costs a handful of inference tokens; + * BM25 costs none, because the engine derives it from collection statistics. + */ +function hybridBody(query: string, limit: number, withPayload: true | string[]) { + return { + prefetch: [ + { + query: { text: query, model: DENSE_MODEL }, + using: DENSE_VECTOR, + limit: PREFETCH_LIMIT, + }, + { + query: { text: query, model: SPARSE_MODEL }, + using: SPARSE_VECTOR, + limit: PREFETCH_LIMIT, + }, + ], + query: { fusion: "rrf" }, + limit, + with_payload: withPayload, + }; +} + +export type SearchOutcome = { + results: NluHit[]; + tokens: number; + timings: { nlu_ms: number; code_ms: number }; +}; + +export async function search(query: string, limit = 5, codeLimit = 20): Promise { + // The two searches share nothing but the query string, so they go out + // together. Serialising them made every search wait for both round trips end + // to end, which on a cluster in another region is most of the response. + const nluStarted = Date.now(); + const nluCall = post<{ points: Scored[] }>( + `/collections/${NLU_COLLECTION}/points/query`, + hybridBody(query, limit, true), + ).then((r) => ({ ...r, ms: Date.now() - nluStarted })); + + const codeStarted = Date.now(); + const codeCall = post<{ points: Scored[] }>( + `/collections/${CODE_COLLECTION}/points/query`, + hybridBody(query, codeLimit, ["start_line", "end_line", "file"]), + ).then((r) => ({ ...r, ms: Date.now() - codeStarted })); + + const [nlu, code] = await Promise.all([nluCall, codeCall]); + + return { + results: mergeSearchResults( + code.result.points.map((p) => p.payload), + nlu.result.points.map((p) => p.payload), + ), + tokens: tokensUsed(nlu.usage) + tokensUsed(code.usage), + timings: { nlu_ms: nlu.ms, code_ms: code.ms }, + }; +} diff --git a/api/file.ts b/api/file.ts new file mode 100644 index 0000000..f570398 --- /dev/null +++ b/api/file.ts @@ -0,0 +1,45 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +import { FILE_COLLECTION } from "./_lib/config.js"; +import { post, QdrantError } from "./_lib/qdrant.js"; + +type FilePayload = { + path: string; + code: string[]; + startline?: number; + endline?: number; +}; + +/** + * The file viewer. A filtered scroll on `path` and nothing else - this + * collection holds no vectors, it is Qdrant used as the demo's only datastore + * so that there is no second thing to deploy. + * + * `path` must have a keyword payload index. Clusters with strict mode on refuse + * to filter an unindexed field, and without it every result anyone clicks + * returns a 500 while search itself keeps working, which reads like a frontend + * bug. indexer/build.py creates it. + */ +export default async function handler(req: VercelRequest, res: VercelResponse) { + const path = typeof req.query.path === "string" ? req.query.path : ""; + if (!path) { + res.status(400).json({ detail: "path is required" }); + return; + } + + try { + const { result } = await post<{ points: { payload: FilePayload }[] }>( + `/collections/${FILE_COLLECTION}/points/scroll`, + { + filter: { must: [{ key: "path", match: { value: path } }] }, + limit: 5, + with_payload: true, + with_vector: false, + }, + ); + res.status(200).json({ result: result.points.map((p) => p.payload) }); + } catch (err) { + const status = err instanceof QdrantError ? err.status : 500; + res.status(status).json({ detail: err instanceof Error ? err.message : String(err) }); + } +} diff --git a/api/health.ts b/api/health.ts new file mode 100644 index 0000000..4bef755 --- /dev/null +++ b/api/health.ts @@ -0,0 +1,48 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +import { + CODE_COLLECTION, + DENSE_MODEL, + FILE_COLLECTION, + NLU_COLLECTION, + SPARSE_MODEL, +} from "./_lib/config.js"; +import { post, QdrantError } from "./_lib/qdrant.js"; + +/** + * Liveness plus the two things that actually break: a collection that is not + * there, and point counts that say an indexing run stopped halfway. Reporting + * only "ok" would have passed on every day the demo was broken. + */ +export default async function handler(_req: VercelRequest, res: VercelResponse) { + const names = { + code: CODE_COLLECTION, + signatures: NLU_COLLECTION, + files: FILE_COLLECTION, + }; + + const collections: Record = {}; + let ok = true; + + await Promise.all( + Object.entries(names).map(async ([label, name]) => { + try { + const { result } = await post<{ count: number }>( + `/collections/${name}/points/count`, + { exact: false }, + ); + collections[label] = result.count; + if (result.count === 0) ok = false; + } catch (err) { + collections[label] = err instanceof QdrantError ? `error: ${err.message}` : "error"; + ok = false; + } + }), + ); + + res.status(ok ? 200 : 503).json({ + status: ok ? "ok" : "degraded", + collections, + models: { dense: DENSE_MODEL, sparse: SPARSE_MODEL }, + }); +} diff --git a/api/search.ts b/api/search.ts new file mode 100644 index 0000000..03e6e43 --- /dev/null +++ b/api/search.ts @@ -0,0 +1,60 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; + +import { cacheGet, cacheKey, cacheSet } from "./_lib/cache.js"; +import { DENSE_MODEL, INDEXED_COMMIT, SPARSE_MODEL } from "./_lib/config.js"; +import { QdrantError } from "./_lib/qdrant.js"; +import { search } from "./_lib/search.js"; + +export default async function handler(req: VercelRequest, res: VercelResponse) { + const query = typeof req.query.query === "string" ? req.query.query.trim() : ""; + if (!query) { + res.status(400).json({ detail: "query is required" }); + return; + } + + // The UI always shows five. A larger limit is allowed so a benchmark can ask + // for a deeper cut without a second deployment, capped so a crawler cannot + // turn one request into a full scan. + const requested = Number(req.query.limit); + const limit = Number.isFinite(requested) ? Math.min(Math.max(requested, 1), 20) : 5; + + // Time the work this function is responsible for: the round trip to Qdrant, + // which now includes the embedding. Network time to the viewer is the + // caller's, and reporting a number that moves with their connection would + // make it meaningless. The UI shows this rather than asserting a figure, so + // it cannot go stale. + const started = Date.now(); + const key = cacheKey(query, limit); + const hit = cacheGet>>(key); + try { + const outcome = hit ?? (await search(query, limit)); + if (!hit) cacheSet(key, outcome); + res.status(200).json({ + result: outcome.results, + latency_ms: Date.now() - started, + indexed_commit: INDEXED_COMMIT, + // Tokens the cluster billed for this search. Zero on a cache hit, because + // nothing was embedded, which is what makes the cache visible in the + // numbers rather than something to take on trust. + inference_tokens: hit ? 0 : outcome.tokens, + cached: Boolean(hit), + // Named here rather than in the UI so the badge cannot drift from what + // actually answered the query. + models: { dense: DENSE_MODEL, sparse: SPARSE_MODEL }, + }); + } catch (err) { + if (err instanceof QdrantError && err.status === 404) { + // A missing collection is a deploy state, not a query with no matches. + // The old backend answered 200 with an empty list here, which is how the + // demo sat broken without anyone noticing. + res.status(503).json({ + detail: + "Search index is unavailable: a Qdrant collection is missing. " + + "Run indexer/build.py to populate it.", + }); + return; + } + const status = err instanceof QdrantError ? err.status : 500; + res.status(status).json({ detail: err instanceof Error ? err.message : String(err) }); + } +} diff --git a/bench/bakeoff.py b/bench/bakeoff.py new file mode 100644 index 0000000..19b2781 --- /dev/null +++ b/bench/bakeoff.py @@ -0,0 +1,259 @@ +"""Pick the encoder for the rebuild, measured on Qdrant Cloud Inference itself. + +The old demo embedded queries with UnixCoder in the backend process. UnixCoder +is not in the Qdrant Cloud Inference catalog and cannot be added, so consolidating onto +one vendor forces an encoder change. This decides which one, and it does it by +running the real production path - the cluster embeds, the cluster searches - +rather than by scoring vectors in a notebook. + +Two query sets, because they disagree and the disagreement is the finding: + + queries.jsonl docstrings lifted from the code they describe, so they + share tokens with the answer. Flatters lexical search. + queries_paraphrase.jsonl the same intent rewritten to avoid the identifiers. + This is what a person actually types. + +Writes one temporary collection and deletes it at the end. Touches nothing the +live demo reads. +""" + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import ( # noqa: E402 + DENSE, + SPARSE, + call, + percentile, + tokens_by_model, +) + +COLLECTION = "bench-code-cloudinf" +EVAL_DIR = os.environ.get("EVAL_DIR", r"C:\Users\Home Laptop\code-search-eval") +RESULTS = os.path.join(os.path.dirname(__file__), "results") + +PREFETCH = 100 +LIMIT = 10 +KS = (1, 5, 10) +BATCH = 16 + + +def load(name): + with open(os.path.join(EVAL_DIR, name), encoding="utf-8") as fp: + return [json.loads(line) for line in fp if line.strip()] + + +def build(corpus): + """Create the bench collection and embed every document with all four models. + + One upsert per batch carries all four vectors, so a document is read once and + the four models see byte-identical input. Any difference in the results below + is the model, not the preprocessing. + """ + call("DELETE", f"/collections/{COLLECTION}") + call( + "PUT", + f"/collections/{COLLECTION}", + { + "vectors": { + name: {"size": size, "distance": "Cosine", "on_disk": True} + for name, (_model, size) in DENSE.items() + }, + # BM25 needs IDF applied at query time; the engine computes it from + # collection statistics, which is why it costs no inference tokens. + "sparse_vectors": { + "bm25": {"modifier": "idf"}, + "splade": {}, + }, + }, + ) + + totals = {} + embed_ms = [] + for start in range(0, len(corpus), BATCH): + batch = corpus[start : start + BATCH] + points = [] + for i, doc in enumerate(batch, start=start): + text = doc["text"] + vector = { + name: {"text": text, "model": model} for name, (model, _s) in DENSE.items() + } + vector.update( + {name: {"text": text, "model": model} for name, model in SPARSE.items()} + ) + points.append( + { + "id": i, + "vector": vector, + "payload": {"doc_id": doc["id"], "file_path": doc["file_path"]}, + } + ) + _res, ms, usage = call("PUT", f"/collections/{COLLECTION}/points?wait=true", {"points": points}) + embed_ms.append(ms) + for model, n in tokens_by_model(usage).items(): + totals[model] = totals.get(model, 0) + n + done = min(start + BATCH, len(corpus)) + if done % 800 == 0 or done == len(corpus): + print(f" indexed {done}/{len(corpus)}", flush=True) + + return totals, embed_ms + + +def query_body(config, text): + """Build the query for one configuration. + + Hybrid runs as a server-side prefetch per leg plus a fusion step, which is + one round trip - the same shape the deployed API will use. + """ + dense, sparse, fusion = config + legs = [] + if dense: + model, _size = DENSE[dense] + legs.append({"query": {"text": text, "model": model}, "using": dense, "limit": PREFETCH}) + if sparse: + legs.append( + {"query": {"text": text, "model": SPARSE[sparse]}, "using": sparse, "limit": PREFETCH} + ) + + if len(legs) == 1: + leg = legs[0] + return {"query": leg["query"], "using": leg["using"], "limit": LIMIT, + "with_payload": ["doc_id"]} + return {"prefetch": legs, "query": {"fusion": fusion}, "limit": LIMIT, + "with_payload": ["doc_id"]} + + +def rank_of(points, answer_id): + for rank, point in enumerate(points, start=1): + if point["payload"]["doc_id"] == answer_id: + return rank + return None + + +def metrics(ranks): + n = len(ranks) + out = {f"recall@{k}": round(sum(1 for r in ranks if r and r <= k) / n, 4) for k in KS} + out["mrr@10"] = round(sum(1.0 / r for r in ranks if r and r <= 10) / n, 4) + return out + + +def evaluate(config, queries): + ranks, latencies, tokens = [], [], 0 + for q in queries: + result, ms, usage = call( + "POST", f"/collections/{COLLECTION}/points/query", query_body(config, q["query"]) + ) + ranks.append(rank_of(result["points"], q["answer_id"])) + latencies.append(ms) + tokens += sum(tokens_by_model(usage).values()) + return { + **metrics(ranks), + "p50_ms": percentile(latencies, 50), + "p95_ms": percentile(latencies, 95), + "tokens": tokens, + } + + +# (dense leg, sparse leg, fusion). RRF ranks by position, so a document has to +# place well in one leg to survive; DBSF normalises the two score distributions +# and adds them, which lets a strong dense score outvote a weak lexical one. +# Both are tested because the paraphrase set is exactly where that difference +# should show: those queries share no identifiers with their answers, so the +# lexical leg is contributing noise and RRF still gives it half the vote. +CONFIGS = [ + ("minilm", None, None), + ("mxbai", None, None), + (None, "bm25", None), + (None, "splade", None), + ("minilm", "bm25", "rrf"), + ("minilm", "splade", "rrf"), + ("mxbai", "bm25", "rrf"), + ("mxbai", "splade", "rrf"), + ("minilm", "bm25", "dbsf"), + ("mxbai", "bm25", "dbsf"), +] + + +def label(config): + dense, sparse, fusion = config + if dense and sparse: + return f"{dense} + {sparse} ({fusion.upper()})" + return f"{dense or sparse} only" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--skip-index", action="store_true", + help="reuse an existing bench collection") + ap.add_argument("--keep", action="store_true", + help="leave the bench collection in place afterwards") + ap.add_argument("--resume", action="store_true", + help="keep configurations already scored in results/bakeoff.json") + args = ap.parse_args() + + corpus = load("corpus.jsonl") + sets = { + "docstring": load("queries.jsonl"), + "paraphrase": load("queries_paraphrase.jsonl"), + } + print(f"{len(corpus)} documents, " + ", ".join(f"{len(v)} {k}" for k, v in sets.items())) + + index_tokens, embed_ms = {}, [] + if not args.skip_index: + print(f"indexing into {COLLECTION} with all four models ...", flush=True) + index_tokens, embed_ms = build(corpus) + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "bakeoff.json") + + # Rows already scored are kept. Eight configurations over two query sets is + # a few thousand requests, and losing the completed half to a reset + # connection in the last one is a bad way to spend an hour. + report = {"corpus_size": len(corpus), "prefetch_per_leg": PREFETCH, "rows": []} + if args.resume and os.path.exists(out): + with open(out, encoding="utf-8") as fp: + report = json.load(fp) + print(f"resuming, {len(report['rows'])} configurations already scored") + if index_tokens: + report["index_tokens"] = index_tokens + report["index_batch_p50_ms"] = percentile(embed_ms, 50) + + scored = {row["config"] for row in report["rows"]} + for config in CONFIGS: + name = label(config) + if name in scored: + continue + row = {"config": name} + for set_name, queries in sets.items(): + print(f" {name:26} {set_name} ...", flush=True) + row[set_name] = evaluate(config, queries) + report["rows"].append(row) + with open(out, "w", encoding="utf-8") as fp: + json.dump(report, fp, indent=2) + + rows = report["rows"] + index_tokens = report.get("index_tokens", index_tokens) + + hdr = (f"{'configuration':26} {'docR@1':>7} {'docR@10':>8} {'docMRR':>7} " + f"{'parR@1':>7} {'parR@10':>8} {'parMRR':>7} {'p50':>7} {'p95':>7}") + print("\n" + hdr) + print("-" * len(hdr)) + for r in rows: + d, p = r["docstring"], r["paraphrase"] + print(f"{r['config']:26} {d['recall@1']:>7.3f} {d['recall@10']:>8.3f} {d['mrr@10']:>7.3f} " + f"{p['recall@1']:>7.3f} {p['recall@10']:>8.3f} {p['mrr@10']:>7.3f} " + f"{d['p50_ms']:>7.1f} {d['p95_ms']:>7.1f}") + print(f"\nindex tokens: {json.dumps(index_tokens)}") + print(f"written to {out}") + + if not args.keep: + call("DELETE", f"/collections/{COLLECTION}") + print(f"dropped {COLLECTION}") + + +if __name__ == "__main__": + main() diff --git a/bench/common.py b/bench/common.py new file mode 100644 index 0000000..eeb85a2 --- /dev/null +++ b/bench/common.py @@ -0,0 +1,190 @@ +"""Shared REST helpers for the benchmarks. + +Deliberately stdlib-only. The benchmark has to be runnable by anyone reviewing +the numbers, on a machine that has nothing installed, and the whole point of +moving to Qdrant Cloud Inference is that the client no longer needs a model runtime. +Pulling in qdrant-client here would hide how little the client actually does. + +Connections are kept alive and reused. The first version opened a fresh TCP and +TLS connection per request, and a full run is a few thousand requests: that +churn produced sporadic `[WinError 10054] connection forcibly closed` failures +that looked like the cluster misbehaving and were this script's own fault. It +also put a TLS handshake inside every latency measurement, which is not +something a real client pays on every search. +""" + +import http.client +import json +import os +import random +import threading +import time +import urllib.parse + +QDRANT_URL = os.environ["QDRANT_URL"].rstrip("/") +QDRANT_API_KEY = os.environ["QDRANT_API_KEY"] + +_PARSED = urllib.parse.urlparse(QDRANT_URL) +_HTTPS = _PARSED.scheme == "https" +_HOST = _PARSED.hostname +_PORT = _PARSED.port or (443 if _HTTPS else 6333) + +# Every model here runs inside the Qdrant cluster. Nothing in this file talks to +# a second vendor, which is the property the rebuild exists to have. +DENSE = { + "minilm": ("sentence-transformers/all-MiniLM-L6-v2", 384), + "mxbai": ("mixedbread-ai/mxbai-embed-large-v1", 1024), +} +SPARSE = { + "bm25": "Qdrant/bm25", + "splade": "prithivida/Splade_PP_en_v1", +} + +# Retried statuses. 429 is the inference rate limit; the 5xxs are the cluster +# briefly refusing work. Both happen over a run of a few thousand requests, and +# neither is a result - a benchmark that dies at row six and reports nothing is +# worse than one that waits a second. +RETRY_STATUS = {429, 500, 502, 503, 504} +MAX_ATTEMPTS = 8 + + +class QdrantError(RuntimeError): + pass + + +class Pool: + """Keep-alive connections to one host, one per thread. + + Used for the Qdrant cluster below and, in latency.py and quality.py, for the + deployed demo. Same reasoning both times: a TLS handshake per request is not + what a browser pays, so including one in every sample would report a number + nobody experiences. + """ + + def __init__(self, base_url, timeout=300): + parsed = urllib.parse.urlparse(base_url) + self.https = parsed.scheme != "http" + self.host = parsed.hostname + self.port = parsed.port or (443 if self.https else 80) + self.timeout = timeout + self._local = threading.local() + + def connection(self, fresh=False): + conn = getattr(self._local, "conn", None) + if conn is not None and fresh: + conn.close() + conn = None + if conn is None: + cls = http.client.HTTPSConnection if self.https else http.client.HTTPConnection + conn = cls(self.host, self.port, timeout=self.timeout) + self._local.conn = conn + return conn + + def get(self, path, headers=None): + """One GET, no retry. Returns (status, body_bytes, elapsed_ms). + + No retry on purpose: this measures a deployment, and silently retrying a + failed request would turn an error rate into a latency figure. + """ + conn = self.connection() + started = time.perf_counter() + try: + conn.request("GET", path, headers=headers or {}) + response = conn.getresponse() + raw = response.read() + return response.status, raw, (time.perf_counter() - started) * 1000 + except (OSError, http.client.HTTPException) as exc: + self.connection(fresh=True) + return 0, str(exc).encode(), (time.perf_counter() - started) * 1000 + + +_pool = Pool(QDRANT_URL) + + +def _connection(fresh=False): + return _pool.connection(fresh=fresh) + + +def call(method: str, path: str, body=None, timeout=300): + """One REST call, retried on transient failures. + + Returns (result, elapsed_ms, usage). elapsed_ms times the successful attempt + only: a latency figure that included backoff would measure this script's + retry policy rather than the search. + """ + data = json.dumps(body).encode() if body is not None else None + headers = {"api-key": QDRANT_API_KEY, "Content-Type": "application/json"} + last = None + + for attempt in range(MAX_ATTEMPTS): + # A connection that failed is not reused. Retrying a request down a + # half-closed socket fails the same way every time, which is how a + # transient blip turns into a run that never recovers. + conn = _connection(fresh=attempt > 0) + started = time.perf_counter() + try: + conn.request(method, path, body=data, headers=headers) + response = conn.getresponse() + raw = response.read() + elapsed = (time.perf_counter() - started) * 1000 + + if response.status in RETRY_STATUS: + last = QdrantError(f"{method} {path} -> {response.status}: {raw.decode()[:400]}") + else: + payload = json.loads(raw) + if "result" not in payload: + raise QdrantError( + f"{method} {path} -> {response.status}: {json.dumps(payload)[:400]}" + ) + return payload["result"], elapsed, payload.get("usage", {}) + except (OSError, http.client.HTTPException, json.JSONDecodeError) as exc: + last = QdrantError(f"{method} {path} -> {type(exc).__name__}: {exc}") + + # Exponential backoff with jitter, but with a floor: pure full jitter + # can pick a delay of almost zero, which retries into the same problem. + time.sleep(min(30.0, 2**attempt) * (0.5 + 0.5 * random.random())) + + raise last + + +def target_headers() -> dict: + """Headers for requests to a deployed demo. + + A Vercel deployment with Deployment Protection on answers 302 to its SSO + page instead of serving the API, which a benchmark would otherwise record as + a fast response to nothing. Setting VERCEL_PROTECTION_BYPASS to the + project's automation bypass secret lets the measurement through while the + URL stays closed to everyone else. + """ + headers = {"accept": "application/json"} + secret = os.environ.get("VERCEL_PROTECTION_BYPASS") + if secret: + headers["x-vercel-protection-bypass"] = secret + # Stops the bypass from setting a cookie that would make later requests + # take a different path through the edge than the first one. + headers["x-vercel-set-bypass-cookie"] = "false" + return headers + + +def tokens_of(usage: dict) -> int: + """Billable tokens reported by Qdrant Cloud Inference for one call. + + Qdrant returns this per model on every request that used inference, which is + the only honest way to price a full index run: measure a sample, multiply. + BM25 is computed in-engine and never appears here, which is itself a result. + """ + models = (usage or {}).get("inference", {}).get("models", {}) + return sum(m.get("tokens", 0) for m in models.values()) + + +def tokens_by_model(usage: dict) -> dict: + models = (usage or {}).get("inference", {}).get("models", {}) + return {name: m.get("tokens", 0) for name, m in models.items()} + + +def percentile(values, p): + if not values: + return 0.0 + ordered = sorted(values) + idx = min(int(round((p / 100) * (len(ordered) - 1))), len(ordered) - 1) + return round(ordered[idx], 1) diff --git a/bench/fusion_check.py b/bench/fusion_check.py new file mode 100644 index 0000000..a374046 --- /dev/null +++ b/bench/fusion_check.py @@ -0,0 +1,164 @@ +"""Is the sparse leg helping or hurting on the deployed corpus? + + python bench/fusion_check.py + +The head-to-head against the old demo came out split: the rebuild wins the +docstring set by seven points of recall@5 and loses the paraphrase set. The +bake-off, on a 5,000-document pool, already hinted at why - mxbai on its own +scored 0.381 paraphrase recall@10 against 0.336 for mxbai fused with BM25. When +a query shares no identifiers with its answer, the lexical leg is not neutral, +it actively pushes wrong documents up. + +This checks that on the real 123k corpus rather than the sample, by querying the +deployed signature collection three ways. It only reads, and it writes nothing. + +The signature collection is the one under test because it is the collection that +produces results. The snippet collection only contributes highlight ranges. +""" + +import json +import os +import sys +import urllib.parse +from concurrent.futures import ThreadPoolExecutor + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import call, percentile # noqa: E402 + +COLLECTION = os.environ.get("QDRANT_NLU_COLLECTION", "code-signatures-cloud") +DENSE_MODEL = os.environ.get("QDRANT_DENSE_MODEL", "mixedbread-ai/mxbai-embed-large-v1") +SPARSE_MODEL = os.environ.get("QDRANT_SPARSE_MODEL", "Qdrant/bm25") + +# The old demo's signature search, reproduced exactly: MiniLM over the +# collection the old deployment still reads. Included so the comparison is +# between two rankings measured the same way on the same day, rather than +# against a figure published earlier under unknown conditions. +OLD_COLLECTION = os.environ.get("OLD_NLU_COLLECTION", "code-signatures") +OLD_MODEL = "sentence-transformers/all-MiniLM-L6-v2" +EVAL_DIR = os.environ.get("EVAL_DIR", r"C:\Users\Home Laptop\code-search-eval") +RESULTS = os.path.join(os.path.dirname(__file__), "results") + +# The demo shows five results, so five is what gets scored. Ranking quality +# below the fold is not what a visitor experiences. +LIMIT = 5 +PREFETCH = 100 +KS = (1, 3, 5) +WORKERS = 4 + + +def load(name): + with open(os.path.join(EVAL_DIR, name), encoding="utf-8") as fp: + return [json.loads(line) for line in fp if line.strip()] + + +def dense_leg(text): + return {"query": {"text": text, "model": DENSE_MODEL}, "using": "dense", "limit": PREFETCH} + + +def sparse_leg(text): + return {"query": {"text": text, "model": SPARSE_MODEL}, "using": "sparse", "limit": PREFETCH} + + +def body(config, text): + payload = ["name", "context"] + if config == "dense only": + leg = dense_leg(text) + return {"query": leg["query"], "using": "dense", "limit": LIMIT, "with_payload": payload} + if config == "sparse only": + leg = sparse_leg(text) + return {"query": leg["query"], "using": "sparse", "limit": LIMIT, "with_payload": payload} + if config == "hybrid RRF": + return {"prefetch": [dense_leg(text), sparse_leg(text)], + "query": {"fusion": "rrf"}, "limit": LIMIT, "with_payload": payload} + # Dense decides the ranking; the sparse leg only widens the candidate pool + # it reranks. A lexical hit can still surface a document, but it cannot + # push one to the top on term overlap alone. + if config == "sparse recall, dense rank": + return {"prefetch": [dense_leg(text), sparse_leg(text)], + "query": {"text": text, "model": DENSE_MODEL}, "using": "dense", + "limit": LIMIT, "with_payload": payload} + if config == "old demo (minilm)": + return {"query": {"text": text, "model": OLD_MODEL}, + "limit": LIMIT, "with_payload": payload} + raise ValueError(config) + + +def collection_for(config): + return OLD_COLLECTION if config == "old demo (minilm)" else COLLECTION + + +CONFIGS = ["dense only", "sparse only", "hybrid RRF", "sparse recall, dense rank", + "old demo (minilm)"] + + +def rank_of(points, expected_file, expected_name): + for rank, point in enumerate(points, start=1): + context = point["payload"].get("context") or {} + if context.get("file_path") != expected_file: + continue + name = point["payload"].get("name") + if not name or not expected_name or name == expected_name: + return rank + return None + + +def evaluate(config, queries): + ranks, times = [None] * len(queries), [] + + collection = collection_for(config) + + def one(i): + result, ms, _u = call("POST", f"/collections/{collection}/points/query", + body(config, queries[i]["query"])) + times.append(ms) + ranks[i] = rank_of(result["points"], queries[i]["file_path"], queries[i].get("name")) + + with ThreadPoolExecutor(max_workers=WORKERS) as pool: + list(pool.map(one, range(len(queries)))) + + n = len(queries) + return { + **{f"recall@{k}": round(sum(1 for r in ranks if r and r <= k) / n, 4) for k in KS}, + "mrr@5": round(sum(1.0 / r for r in ranks if r and r <= LIMIT) / n, 4), + "p50_ms": percentile(times, 50), + # Kept so two configurations can be compared query by query. A + # difference in the averages says nothing about whether the two rankings + # actually differ on this many queries. + "ranks": ranks, + } + + +def main(): + sets = {"docstring": load("queries.jsonl"), "paraphrase": load("queries_paraphrase.jsonl")} + print(f"{COLLECTION}, top {LIMIT}, " + + ", ".join(f"{len(v)} {k}" for k, v in sets.items())) + + report = {} + for config in CONFIGS: + report[config] = {} + for set_name, queries in sets.items(): + report[config][set_name] = evaluate(config, queries) + d, p = report[config]["docstring"], report[config]["paraphrase"] + print(f" {config:28} doc R@5 {d['recall@5']:.3f} MRR {d['mrr@5']:.3f} | " + f"par R@5 {p['recall@5']:.3f} MRR {p['mrr@5']:.3f}", flush=True) + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "fusion_check.json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(report, fp, indent=2) + + header = (f"{'configuration':28} {'docR@1':>7} {'docR@5':>7} {'docMRR':>7} " + f"{'parR@1':>7} {'parR@5':>7} {'parMRR':>7} {'p50':>6}") + print("\n" + header) + print("-" * len(header)) + for config in CONFIGS: + d, p = report[config]["docstring"], report[config]["paraphrase"] + print(f"{config:28} {d['recall@1']:>7.3f} {d['recall@5']:>7.3f} {d['mrr@5']:>7.3f} " + f"{p['recall@1']:>7.3f} {p['recall@5']:>7.3f} {p['mrr@5']:>7.3f} " + f"{d['p50_ms']:>6.0f}") + print(f"\nwritten to {out}") + + +if __name__ == "__main__": + main() diff --git a/bench/index_cost.py b/bench/index_cost.py new file mode 100644 index 0000000..15d313c --- /dev/null +++ b/bench/index_cost.py @@ -0,0 +1,111 @@ +"""What a full indexing run costs, measured rather than guessed. + + python bench/index_cost.py # sample of 300 docs + python bench/index_cost.py --sample 1000 --corpus-size 140444 + +Qdrant Cloud Inference bills per million tokens and reports the tokens it used on every +response. So the cost of embedding 140k chunks is a sample of a few hundred, +multiplied. Sampling is uniform across the corpus rather than taking the first N, +because the first N are alphabetically clustered and code snippet lengths are not +evenly distributed across a repository. + +Writes one small collection and deletes it. Prices are not hardcoded: the +catalog changes and a stale number in a repository is worse than no number, so +this reports tokens and leaves the multiplication to whoever reads the Inference +tab of the cluster page. +""" + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import DENSE, SPARSE, call, percentile, tokens_by_model # noqa: E402 + +COLLECTION = "bench-index-cost" +EVAL_DIR = os.environ.get("EVAL_DIR", r"C:\Users\Home Laptop\code-search-eval") +RESULTS = os.path.join(os.path.dirname(__file__), "results") + +# The two collections a real run builds, and how many points each holds today. +CORPUS = {"code-snippets": 123257, "code-signatures": 17187} +BATCH = 16 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--sample", type=int, default=300) + ap.add_argument("--corpus-size", type=int, default=sum(CORPUS.values()), + help="points a full run would embed") + args = ap.parse_args() + + with open(os.path.join(EVAL_DIR, "corpus.jsonl"), encoding="utf-8") as fp: + corpus = [json.loads(line) for line in fp if line.strip()] + + step = max(1, len(corpus) // args.sample) + sample = corpus[::step][: args.sample] + print(f"sampling {len(sample)} of {len(corpus)} documents, every {step}th") + + call("DELETE", f"/collections/{COLLECTION}") + call("PUT", f"/collections/{COLLECTION}", { + "vectors": {n: {"size": s, "distance": "Cosine"} for n, (_m, s) in DENSE.items()}, + "sparse_vectors": {"bm25": {"modifier": "idf"}, "splade": {}}, + }) + + totals, batch_ms = {}, [] + try: + for start in range(0, len(sample), BATCH): + points = [] + for i, doc in enumerate(sample[start : start + BATCH], start=start): + vector = {n: {"text": doc["text"], "model": m} for n, (m, _s) in DENSE.items()} + vector.update({n: {"text": doc["text"], "model": m} for n, m in SPARSE.items()}) + points.append({"id": i, "vector": vector, "payload": {}}) + _res, ms, usage = call("PUT", f"/collections/{COLLECTION}/points?wait=true", + {"points": points}) + batch_ms.append(ms) + for model, n in tokens_by_model(usage).items(): + totals[model] = totals.get(model, 0) + n + finally: + call("DELETE", f"/collections/{COLLECTION}") + + n = len(sample) + report = { + "sampled": n, + "corpus_size": args.corpus_size, + "batch_p50_ms": percentile(batch_ms, 50), + "models": { + model: { + "tokens_per_doc": round(tokens / n, 1), + "projected_tokens": round(tokens / n * args.corpus_size), + "projected_millions": round(tokens / n * args.corpus_size / 1e6, 2), + } + for model, tokens in sorted(totals.items()) + }, + } + # Any model that was sent input but reported no tokens billed nothing. BM25 + # is the one that matters: the engine computes it from collection + # statistics, so the sparse half of a hybrid index is free however large the + # corpus gets. Compared on full model ids, lowercased, because that is what + # the usage block echoes back. + sent = {model.lower() for model, _dim in DENSE.values()} | { + model.lower() for model in SPARSE.values() + } + report["free"] = sorted(sent - {k.lower() for k in totals}) + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "index_cost.json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(report, fp, indent=2) + + print(f"\n{'model':40} {'tok/doc':>8} {'for ' + str(args.corpus_size):>14}") + print("-" * 66) + for model, row in report["models"].items(): + print(f"{model:40} {row['tokens_per_doc']:>8.1f} {row['projected_millions']:>11.2f} M") + print(f"\nbilled nothing: {', '.join(report['free']) or 'none'}") + print(f"written to {out}") + + +if __name__ == "__main__": + main() diff --git a/bench/latency.py b/bench/latency.py new file mode 100644 index 0000000..17867fd --- /dev/null +++ b/bench/latency.py @@ -0,0 +1,160 @@ +"""Latency of a deployed search endpoint, and of the Qdrant leg underneath it. + + python bench/latency.py --target https://demo-code-search-cloud.vercel.app + python bench/latency.py --target http://127.0.0.1:3000 --n 100 + python bench/latency.py --target --compare https://code-search.qdrant.tech + +Reports three numbers that are usually conflated: + + end-to-end what a viewer waits for, measured from this machine + server what the function reports it spent talking to Qdrant + overhead the difference, which is the function, Vercel's edge, and the + network between here and there + +Splitting them matters because the rebuild moved the embedding from the backend +process into the cluster. If only the total is reported, a slower network reads +as a slower search and an improvement in the search reads as noise. + +The first request is reported on its own rather than averaged in. It pays the +connection setup and any serverless cold start, which is a different event from +the hundredth request, and burying it in a p50 hides the one number people ask +about when they hear the backend is serverless. +""" + +import argparse +import json +import os +import statistics +import sys +import urllib.parse + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import Pool, percentile, target_headers # noqa: E402 + +RESULTS = os.path.join(os.path.dirname(__file__), "results") + +# Queries the demo itself suggests, plus a few that are deliberately awkward: +# an exact identifier, a misspelling, and questions with no lexical overlap with +# any answer. A latency table built only from the happy path is a table of +# cache hits. +QUERIES = [ + "cardinality of should request", + "how to calculate the size of a quantized vector", + "flush the write ahead log to disk", + "estimate_cardinality", + "recomend points based on positive and negative examples", + "what happens when a shard is transferred to another node", + "convert grpc filter into internal representation", + "throttle the optimizer so it does not eat the machine", + "read a segment while it is being written", + "where are payload indexes persisted", +] + + +def normalise(base): + """Split a base URL into a Pool and the path prefix to prepend.""" + url = base if "://" in base else f"https://{base}" + parsed = urllib.parse.urlparse(url) + return Pool(url, timeout=90), parsed.path.rstrip("/") + + +def search(pool, prefix, query): + """One search against the deployment. Returns (body, status, elapsed_ms). + + The connection is kept alive across calls, so these samples measure the + search rather than a TLS handshake - which on a cross-region link is larger + than most of what is being compared. + """ + path = f"{prefix}/api/search?query={urllib.parse.quote(query)}" + status, raw, ms = pool.get(path, target_headers()) + try: + body = json.loads(raw) + except (ValueError, UnicodeDecodeError): + body = {"detail": raw[:200].decode(errors="replace")} + return body, status, ms + + +def measure(base, n, label): + print(f"\n{label} {base}") + pool, prefix = normalise(base) + + first, status, first_ms = search(pool, prefix, "cold start probe, not counted below") + if status != 200: + print(f" FAILED {status}: {json.dumps(first)[:200]}") + return None + print(f" first request (connection setup and any cold start): {first_ms:.0f} ms") + + end_to_end, server, errors, tokens = [], [], 0, 0 + for i in range(n): + # Vary the query so the run measures search rather than a warm cache. + # Repeating one string would report how fast Qdrant returns something it + # has already computed, which is not what anyone is asking about. + suffix = i // len(QUERIES) + query = f"{QUERIES[i % len(QUERIES)]} {suffix or ''}".strip() + body, status, ms = search(pool, prefix, query) + if status != 200: + errors += 1 + continue + end_to_end.append(ms) + if isinstance(body.get("latency_ms"), (int, float)): + server.append(float(body["latency_ms"])) + tokens += body.get("inference_tokens") or 0 + + if not end_to_end: + print(f" every request failed ({errors})") + return None + + row = { + "target": base, + "requests": len(end_to_end), + "errors": errors, + "first_request_ms": round(first_ms), + "end_to_end": { + "p50": percentile(end_to_end, 50), + "p95": percentile(end_to_end, 95), + "mean": round(statistics.fmean(end_to_end), 1), + }, + "inference_tokens_total": tokens, + } + if server: + row["server"] = {"p50": percentile(server, 50), "p95": percentile(server, 95)} + row["overhead_p50"] = round(row["end_to_end"]["p50"] - row["server"]["p50"], 1) + + print(f" end-to-end p50 {row['end_to_end']['p50']:>7.1f} ms " + f"p95 {row['end_to_end']['p95']:>7.1f} ms") + if server: + print(f" server p50 {row['server']['p50']:>7.1f} ms " + f"p95 {row['server']['p95']:>7.1f} ms") + print(f" overhead p50 {row['overhead_p50']:>7.1f} ms") + else: + print(" server not reported by this endpoint") + if tokens: + print(f" inference tokens over {len(end_to_end)} searches: {tokens}") + if errors: + print(f" {errors} request(s) failed") + return row + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--target", required=True, help="base URL of the rebuilt demo") + ap.add_argument("--compare", help="base URL of another deployment, e.g. the old demo") + ap.add_argument("--n", type=int, default=50, help="requests per target") + args = ap.parse_args() + + rows = [r for r in [ + measure(args.target, args.n, "rebuild"), + measure(args.compare, args.n, "comparison") if args.compare else None, + ] if r] + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "latency.json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(rows, fp, indent=2) + print(f"\nwritten to {out}") + + +if __name__ == "__main__": + main() diff --git a/bench/quality.py b/bench/quality.py new file mode 100644 index 0000000..666b04a --- /dev/null +++ b/bench/quality.py @@ -0,0 +1,156 @@ +"""Score a deployed demo end to end, on the same queries as the old one. + + python bench/quality.py --target https://demo-code-search-cloud.vercel.app + python bench/quality.py --target --compare https://code-search.qdrant.tech + +This is the number that decides whether the rebuild is allowed to ship: the old +demo scored 0.907 file-level recall@10 over the full 123k corpus, and a +consolidation that quietly costs a tenth of the answers is not a consolidation +worth having. + +Both endpoints return five results per search, so recall@10 cannot exceed +recall@5 for either of them. That is a property of the demo, not of the models, +and it is left alone here so the two runs stay comparable. + +Two query sets, and they disagree: + + queries.jsonl docstrings taken from the code they describe, so they + share identifiers with the answer + queries_paraphrase.jsonl the same intent rewritten to avoid those identifiers, + which is closer to what a person types +""" + +import argparse +import json +import os +import sys +import urllib.parse +from concurrent.futures import ThreadPoolExecutor + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import Pool, percentile, target_headers # noqa: E402 + +EVAL_DIR = os.environ.get("EVAL_DIR", r"C:\Users\Home Laptop\code-search-eval") +RESULTS = os.path.join(os.path.dirname(__file__), "results") +KS = (1, 5, 10) + + +def load(name): + with open(os.path.join(EVAL_DIR, name), encoding="utf-8") as fp: + return [json.loads(line) for line in fp if line.strip()] + + +def search(pool, prefix, query): + path = f"{prefix}/api/search?query={urllib.parse.quote(query)}" + status, raw, ms = pool.get(path, target_headers()) + if status != 200: + raise RuntimeError(f"HTTP {status}: {raw[:200].decode(errors='replace')}") + return json.loads(raw).get("result", []), ms + + +def ranks_for(hits, expected_file, expected_name): + """Rank of the right answer, strictly and by file alone. + + Strict means the hit names the same function in the same file. File-only + means the search landed in the right file, which is what the old demo's + published figure measured, so both are kept rather than picking one. + """ + strict = loose = None + for rank, hit in enumerate(hits, start=1): + context = hit.get("context") or {} + path = context.get("file_path") or hit.get("file") + if path != expected_file: + continue + if loose is None: + loose = rank + name = hit.get("name") + if strict is None and (not name or not expected_name or name == expected_name): + strict = rank + return strict, loose + + +def score(ranks, n): + out = {f"recall@{k}": round(sum(1 for r in ranks if r and r <= k) / n, 4) for k in KS} + out["mrr@10"] = round(sum(1.0 / r for r in ranks if r and r <= 10) / n, 4) + return out + + +def evaluate(base, queries, workers): + strict, loose, times = [None] * len(queries), [None] * len(queries), [] + errors = [] + url = base if "://" in base else f"https://{base}" + # One Pool shared by every worker; it hands out a connection per thread, so + # `workers` connections are opened once instead of one per request. + http = Pool(url, timeout=90) + prefix = urllib.parse.urlparse(url).path.rstrip("/") + + def one(i): + q = queries[i] + try: + hits, ms = search(http, prefix, q["query"]) + except (OSError, ValueError, RuntimeError) as exc: + errors.append(f"{i}: {exc}") + return + times.append(ms) + strict[i], loose[i] = ranks_for(hits, q["file_path"], q.get("name")) + + with ThreadPoolExecutor(max_workers=workers) as executor: + list(executor.map(one, range(len(queries)))) + + n = len(queries) + return { + "queries": n, + "errors": len(errors), + "error_sample": errors[:3], + "strict": score(strict, n), + "file_only": score(loose, n), + # Measured with `workers` requests in flight, so this is throughput + # latency, not the single-user figure. bench/latency.py measures that. + "concurrent_p50_ms": percentile(times, 50), + "concurrent_p95_ms": percentile(times, 95), + } + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--target", required=True) + ap.add_argument("--compare", help="another deployment to score on the same queries") + ap.add_argument("--workers", type=int, default=4) + ap.add_argument("--limit", type=int, help="use only the first N queries of each set") + args = ap.parse_args() + + sets = {"docstring": load("queries.jsonl"), "paraphrase": load("queries_paraphrase.jsonl")} + if args.limit: + sets = {k: v[: args.limit] for k, v in sets.items()} + + report = {} + for label, base in [("rebuild", args.target), ("comparison", args.compare)]: + if not base: + continue + print(f"\n{label}: {base}") + report[label] = {"target": base} + for set_name, queries in sets.items(): + print(f" {set_name} ({len(queries)} queries) ...", flush=True) + report[label][set_name] = evaluate(base, queries, args.workers) + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "quality.json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(report, fp, indent=2) + + hdr = f"{'target':12} {'set':11} {'R@1':>7} {'R@5':>7} {'fileR@5':>8} {'MRR':>7} {'err':>4}" + print("\n" + hdr) + print("-" * len(hdr)) + for label, row in report.items(): + for set_name in sets: + r = row[set_name] + print(f"{label:12} {set_name:11} {r['strict']['recall@1']:>7.3f} " + f"{r['strict']['recall@5']:>7.3f} {r['file_only']['recall@5']:>8.3f} " + f"{r['strict']['mrr@10']:>7.3f} {r['errors']:>4}") + print(f"\nwritten to {out}") + + +if __name__ == "__main__": + main() diff --git a/bench/results/bakeoff.json b/bench/results/bakeoff.json new file mode 100644 index 0000000..9f218e7 --- /dev/null +++ b/bench/results/bakeoff.json @@ -0,0 +1,216 @@ +{ + "corpus_size": 5000, + "prefetch_per_leg": 100, + "rows": [ + { + "config": "minilm only", + "docstring": { + "recall@1": 0.6133, + "recall@5": 0.8067, + "recall@10": 0.8633, + "mrr@10": 0.6935, + "p50_ms": 53.1, + "p95_ms": 176.0, + "tokens": 4415 + }, + "paraphrase": { + "recall@1": 0.0708, + "recall@5": 0.2124, + "recall@10": 0.3274, + "mrr@10": 0.1372, + "p50_ms": 53.0, + "p95_ms": 205.0, + "tokens": 1366 + } + }, + { + "config": "mxbai only", + "docstring": { + "recall@1": 0.8167, + "recall@5": 0.9467, + "recall@10": 0.9733, + "mrr@10": 0.8759, + "p50_ms": 112.1, + "p95_ms": 124.8, + "tokens": 7415 + }, + "paraphrase": { + "recall@1": 0.1062, + "recall@5": 0.2655, + "recall@10": 0.3805, + "mrr@10": 0.1754, + "p50_ms": 111.4, + "p95_ms": 116.3, + "tokens": 2496 + } + }, + { + "config": "bm25 only", + "docstring": { + "recall@1": 0.8933, + "recall@5": 0.9733, + "recall@10": 0.9933, + "mrr@10": 0.9318, + "p50_ms": 32.0, + "p95_ms": 34.2, + "tokens": 0 + }, + "paraphrase": { + "recall@1": 0.0354, + "recall@5": 0.1239, + "recall@10": 0.1416, + "mrr@10": 0.0683, + "p50_ms": 31.9, + "p95_ms": 33.0, + "tokens": 0 + } + }, + { + "config": "splade only", + "docstring": { + "recall@1": 0.9033, + "recall@5": 0.9733, + "recall@10": 0.9933, + "mrr@10": 0.9347, + "p50_ms": 110.1, + "p95_ms": 141.3, + "tokens": 5015 + }, + "paraphrase": { + "recall@1": 0.0354, + "recall@5": 0.1239, + "recall@10": 0.1327, + "mrr@10": 0.068, + "p50_ms": 108.0, + "p95_ms": 118.1, + "tokens": 1592 + } + }, + { + "config": "minilm + bm25 (RRF)", + "docstring": { + "recall@1": 0.7867, + "recall@5": 0.9633, + "recall@10": 0.9833, + "mrr@10": 0.87, + "p50_ms": 55.5, + "p95_ms": 235.4, + "tokens": 4415 + }, + "paraphrase": { + "recall@1": 0.0708, + "recall@5": 0.2389, + "recall@10": 0.2832, + "mrr@10": 0.1341, + "p50_ms": 55.5, + "p95_ms": 190.7, + "tokens": 1366 + } + }, + { + "config": "minilm + splade (RRF)", + "docstring": { + "recall@1": 0.7867, + "recall@5": 0.9567, + "recall@10": 0.9833, + "mrr@10": 0.87, + "p50_ms": 113.3, + "p95_ms": 312.0, + "tokens": 9430 + }, + "paraphrase": { + "recall@1": 0.0885, + "recall@5": 0.2212, + "recall@10": 0.292, + "mrr@10": 0.1437, + "p50_ms": 114.3, + "p95_ms": 257.0, + "tokens": 2958 + } + }, + { + "config": "mxbai + bm25 (RRF)", + "docstring": { + "recall@1": 0.8767, + "recall@5": 0.9733, + "recall@10": 0.9933, + "mrr@10": 0.9239, + "p50_ms": 116.5, + "p95_ms": 157.2, + "tokens": 7415 + }, + "paraphrase": { + "recall@1": 0.1327, + "recall@5": 0.2478, + "recall@10": 0.3363, + "mrr@10": 0.1778, + "p50_ms": 114.8, + "p95_ms": 120.9, + "tokens": 2496 + } + }, + { + "config": "mxbai + splade (RRF)", + "docstring": { + "recall@1": 0.8767, + "recall@5": 0.9867, + "recall@10": 1.0, + "mrr@10": 0.9252, + "p50_ms": 117.9, + "p95_ms": 164.3, + "tokens": 12430 + }, + "paraphrase": { + "recall@1": 0.1239, + "recall@5": 0.2301, + "recall@10": 0.3363, + "mrr@10": 0.1712, + "p50_ms": 118.9, + "p95_ms": 168.5, + "tokens": 4088 + } + }, + { + "config": "minilm + bm25 (DBSF)", + "docstring": { + "recall@1": 0.8433, + "recall@5": 0.9533, + "recall@10": 0.98, + "mrr@10": 0.8965, + "p50_ms": 56.3, + "p95_ms": 265.4, + "tokens": 4415 + }, + "paraphrase": { + "recall@1": 0.0973, + "recall@5": 0.1947, + "recall@10": 0.2389, + "mrr@10": 0.1396, + "p50_ms": 55.6, + "p95_ms": 155.3, + "tokens": 1366 + } + }, + { + "config": "mxbai + bm25 (DBSF)", + "docstring": { + "recall@1": 0.8967, + "recall@5": 0.9733, + "recall@10": 0.99, + "mrr@10": 0.9325, + "p50_ms": 118.6, + "p95_ms": 186.2, + "tokens": 7415 + }, + "paraphrase": { + "recall@1": 0.1327, + "recall@5": 0.2212, + "recall@10": 0.292, + "mrr@10": 0.1673, + "p50_ms": 117.2, + "p95_ms": 188.7, + "tokens": 2496 + } + } + ] +} \ No newline at end of file diff --git a/bench/results/fusion_check.json b/bench/results/fusion_check.json new file mode 100644 index 0000000..c10dfd2 --- /dev/null +++ b/bench/results/fusion_check.json @@ -0,0 +1,2167 @@ +{ + "dense only": { + "docstring": { + "recall@1": 0.83, + "recall@3": 0.9533, + "recall@5": 0.9667, + "mrr@5": 0.8898, + "p50_ms": 111.0, + "ranks": [ + 2, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 2, + 1, + 4, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 1, + 3, + 1, + null, + 2, + null, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 4, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 3, + 2, + null, + 1, + 1, + 1, + 2, + 3, + 1, + 1, + 1, + 2, + 1, + 1, + 5, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 2, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 4, + 1 + ] + }, + "paraphrase": { + "recall@1": 0.1062, + "recall@3": 0.1593, + "recall@5": 0.2212, + "mrr@5": 0.1454, + "p50_ms": 110.3, + "ranks": [ + null, + null, + null, + 1, + 4, + null, + 1, + null, + null, + null, + 2, + null, + 1, + null, + null, + null, + null, + null, + 5, + null, + null, + null, + null, + 1, + null, + null, + 2, + 3, + 5, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + 1, + null, + null, + null, + 1, + null, + null, + 2, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 5, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + 1, + null, + null, + null, + 4, + 4, + null, + null, + null, + null + ] + } + }, + "sparse only": { + "docstring": { + "recall@1": 0.88, + "recall@3": 0.9733, + "recall@5": 0.98, + "mrr@5": 0.9254, + "p50_ms": 32.1, + "ranks": [ + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 5, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + null, + 1, + 3, + 1, + 1, + 4, + 1, + 2, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 2, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + }, + "paraphrase": { + "recall@1": 0.0265, + "recall@3": 0.0354, + "recall@5": 0.0354, + "mrr@5": 0.031, + "p50_ms": 32.3, + "ranks": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + } + }, + "hybrid RRF": { + "docstring": { + "recall@1": 0.8867, + "recall@3": 0.9767, + "recall@5": 0.9833, + "mrr@5": 0.9306, + "p50_ms": 112.0, + "ranks": [ + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 1, + 3, + 1, + 1, + 1, + null, + 1, + 2, + 1, + 1, + 4, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 4, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 2, + 2, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + }, + "paraphrase": { + "recall@1": 0.0885, + "recall@3": 0.1504, + "recall@5": 0.1858, + "mrr@5": 0.1245, + "p50_ms": 113.1, + "ranks": [ + null, + null, + null, + 1, + null, + null, + 1, + null, + null, + null, + 1, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + 4, + 5, + 5, + 1, + null, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + 1, + null, + null, + null, + 1, + null, + null, + 4, + null, + 2, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + } + }, + "sparse recall, dense rank": { + "docstring": { + "recall@1": 0.83, + "recall@3": 0.9533, + "recall@5": 0.9667, + "mrr@5": 0.8893, + "p50_ms": 109.8, + "ranks": [ + 2, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 2, + 1, + 4, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 1, + 3, + 1, + null, + 2, + null, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 4, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 3, + 2, + null, + 1, + 1, + 1, + 2, + 3, + 1, + 1, + 1, + 2, + 1, + 1, + 5, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 3, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 4, + 1 + ] + }, + "paraphrase": { + "recall@1": 0.1062, + "recall@3": 0.1593, + "recall@5": 0.2212, + "mrr@5": 0.1459, + "p50_ms": 110.0, + "ranks": [ + null, + null, + null, + 1, + 4, + null, + 1, + null, + null, + null, + 2, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + 2, + 3, + 5, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 4, + null, + 1, + null, + null, + null, + 1, + null, + null, + 2, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 5, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + 4, + null, + null, + null, + null, + null, + 2, + null, + 1, + null, + null, + null, + 4, + 4, + null, + null, + null, + null + ] + } + }, + "old demo (minilm)": { + "docstring": { + "recall@1": 0.73, + "recall@3": 0.8833, + "recall@5": 0.9067, + "mrr@5": 0.8057, + "p50_ms": 52.6, + "ranks": [ + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 2, + 1, + 1, + 2, + 1, + 5, + 1, + 1, + 4, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 2, + 1, + 1, + 1, + 1, + null, + null, + 1, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 3, + 1, + null, + 2, + null, + 1, + 1, + 1, + null, + 2, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + null, + 1, + 2, + 1, + 2, + 2, + 1, + 1, + 1, + 2, + 2, + 2, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 2, + 2, + 3, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 2, + 2, + null, + 2, + 1, + 3, + 1, + 4, + 2, + null, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 3, + 1, + 1, + 2, + 1, + null, + 2, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 2, + 1, + 2, + 1, + 1, + 1, + 3, + 1, + 1, + null, + 4, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + 3, + 1, + 1, + 1, + 1, + 4, + 1, + 1, + null, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1, + 1, + 1, + 1, + 4, + 1, + 1, + 1, + 2, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 3, + 4, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ] + }, + "paraphrase": { + "recall@1": 0.0973, + "recall@3": 0.177, + "recall@5": 0.1947, + "mrr@5": 0.1363, + "p50_ms": 52.3, + "ranks": [ + null, + 2, + null, + 1, + 1, + null, + null, + null, + 5, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + null, + null, + 3, + 1, + 3, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 3, + null, + null, + 1, + null, + null, + 2, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 2, + null, + 2, + null, + null, + null, + 5, + null, + null, + null, + null + ] + } + } +} \ No newline at end of file diff --git a/bench/results/index_cost.json b/bench/results/index_cost.json new file mode 100644 index 0000000..628a066 --- /dev/null +++ b/bench/results/index_cost.json @@ -0,0 +1,25 @@ +{ + "sampled": 320, + "corpus_size": 140444, + "batch_p50_ms": 991.2, + "models": { + "mixedbread-ai/mxbai-embed-large-v1": { + "tokens_per_doc": 466.1, + "projected_tokens": 65453926, + "projected_millions": 65.45 + }, + "prithivida/splade_pp_en_v1": { + "tokens_per_doc": 128.0, + "projected_tokens": 17976832, + "projected_millions": 17.98 + }, + "sentence-transformers/all-minilm-l6-v2": { + "tokens_per_doc": 459.4, + "projected_tokens": 64515585, + "projected_millions": 64.52 + } + }, + "free": [ + "qdrant/bm25" + ] +} \ No newline at end of file diff --git a/bench/results/latency.json b/bench/results/latency.json new file mode 100644 index 0000000..692426a --- /dev/null +++ b/bench/results/latency.json @@ -0,0 +1,36 @@ +[ + { + "target": "https://demo-code-search-cloud.vercel.app", + "requests": 60, + "errors": 0, + "first_request_ms": 775, + "end_to_end": { + "p50": 147.8, + "p95": 205.0, + "mean": 159.0 + }, + "inference_tokens_total": 2260, + "server": { + "p50": 91.0, + "p95": 145.0 + }, + "overhead_p50": 56.8 + }, + { + "target": "https://demo-code-search-production.up.railway.app", + "requests": 60, + "errors": 0, + "first_request_ms": 2033, + "end_to_end": { + "p50": 178.0, + "p95": 186.3, + "mean": 178.0 + }, + "inference_tokens_total": 0, + "server": { + "p50": 80.0, + "p95": 84.0 + }, + "overhead_p50": 98.0 + } +] \ No newline at end of file diff --git a/bench/results/quality.json b/bench/results/quality.json new file mode 100644 index 0000000..601770b --- /dev/null +++ b/bench/results/quality.json @@ -0,0 +1,84 @@ +{ + "rebuild": { + "target": "https://demo-code-search-cloud-2n1caqrqm-jkupchankos-projects.vercel.app", + "docstring": { + "queries": 300, + "errors": 0, + "error_sample": [], + "strict": { + "recall@1": 0.8833, + "recall@5": 0.9833, + "recall@10": 0.9833, + "mrr@10": 0.9279 + }, + "file_only": { + "recall@1": 0.9067, + "recall@5": 0.9833, + "recall@10": 0.9833, + "mrr@10": 0.9407 + }, + "concurrent_p50_ms": 237.8, + "concurrent_p95_ms": 405.8 + }, + "paraphrase": { + "queries": 113, + "errors": 0, + "error_sample": [], + "strict": { + "recall@1": 0.0885, + "recall@5": 0.177, + "recall@10": 0.177, + "mrr@10": 0.1235 + }, + "file_only": { + "recall@1": 0.1416, + "recall@5": 0.2655, + "recall@10": 0.2655, + "mrr@10": 0.1879 + }, + "concurrent_p50_ms": 239.0, + "concurrent_p95_ms": 369.8 + } + }, + "comparison": { + "target": "https://demo-code-search-production.up.railway.app", + "docstring": { + "queries": 300, + "errors": 0, + "error_sample": [], + "strict": { + "recall@1": 0.5667, + "recall@5": 0.9067, + "recall@10": 0.9067, + "mrr@10": 0.7131 + }, + "file_only": { + "recall@1": 0.6933, + "recall@5": 0.9133, + "recall@10": 0.9133, + "mrr@10": 0.7899 + }, + "concurrent_p50_ms": 496.9, + "concurrent_p95_ms": 620.2 + }, + "paraphrase": { + "queries": 113, + "errors": 0, + "error_sample": [], + "strict": { + "recall@1": 0.1062, + "recall@5": 0.1947, + "recall@10": 0.1947, + "mrr@10": 0.1423 + }, + "file_only": { + "recall@1": 0.177, + "recall@5": 0.2566, + "recall@10": 0.2566, + "mrr@10": 0.2124 + }, + "concurrent_p50_ms": 504.3, + "concurrent_p95_ms": 675.7 + } + } +} \ No newline at end of file diff --git a/bench/results/truncation.json b/bench/results/truncation.json new file mode 100644 index 0000000..5441e42 --- /dev/null +++ b/bench/results/truncation.json @@ -0,0 +1,37 @@ +{ + "char_budget": 700, + "documents_shortened": 1801, + "mean_tokens_per_doc": 242.7, + "worst_batch_mean_tokens": 341.6, + "under_budget": false, + "sets": { + "dense/docstring": { + "recall@1": 0.72, + "recall@5": 0.8833, + "recall@10": 0.9367, + "mrr@10": 0.7911, + "p50_ms": 52.9 + }, + "dense/paraphrase": { + "recall@1": 0.1062, + "recall@5": 0.2389, + "recall@10": 0.3097, + "mrr@10": 0.1666, + "p50_ms": 53.0 + }, + "hybrid/docstring": { + "recall@1": 0.8667, + "recall@5": 0.9667, + "recall@10": 0.9833, + "mrr@10": 0.9133, + "p50_ms": 53.7 + }, + "hybrid/paraphrase": { + "recall@1": 0.0354, + "recall@5": 0.2478, + "recall@10": 0.3186, + "mrr@10": 0.1258, + "p50_ms": 54.8 + } + } +} \ No newline at end of file diff --git a/bench/significance.py b/bench/significance.py new file mode 100644 index 0000000..623d8ce --- /dev/null +++ b/bench/significance.py @@ -0,0 +1,110 @@ +"""Is a difference between two configurations real, or is it 113 queries? + + python bench/significance.py "hybrid RRF" "old demo (minilm)" + python bench/significance.py "dense only" "hybrid RRF" --set docstring + +Reads the per-query ranks bench/fusion_check.py saved and compares two +configurations query by query, which the averages cannot do. A gap of 0.02 MRR +across 113 queries can be one ranking genuinely beating another, or it can be +four queries landing differently. + +Two things are reported. A paired bootstrap over the query set gives a +confidence interval on the difference: if it straddles zero, the two are not +distinguishable on this many queries. An exact sign test over the queries where +the two disagree gives the probability of seeing a split that lopsided by +chance. + +Stdlib only, and it reads a file rather than the network, so it is instant and +repeatable. +""" + +import argparse +import json +import math +import os +import random +import sys + +RESULTS = os.path.join(os.path.dirname(__file__), "results") +LIMIT = 5 +BOOTSTRAP = 20000 +SEED = 20260901 + + +def reciprocal(rank): + return 1.0 / rank if rank and rank <= LIMIT else 0.0 + + +def mrr(ranks): + return sum(reciprocal(r) for r in ranks) / len(ranks) + + +def bootstrap(a, b, rounds, rng): + """Paired bootstrap over queries. Returns the 95% interval on mrr(a) - mrr(b).""" + n = len(a) + diffs = [] + for _ in range(rounds): + idx = [rng.randrange(n) for _ in range(n)] + diffs.append( + sum(reciprocal(a[i]) for i in idx) / n - sum(reciprocal(b[i]) for i in idx) / n + ) + diffs.sort() + return diffs[int(0.025 * rounds)], diffs[int(0.975 * rounds)] + + +def sign_test(a, b): + """Two-sided exact sign test over the queries where the two rankings differ.""" + wins = sum(1 for x, y in zip(a, b) if reciprocal(x) > reciprocal(y)) + losses = sum(1 for x, y in zip(a, b) if reciprocal(x) < reciprocal(y)) + n = wins + losses + if n == 0: + return wins, losses, 1.0 + k = min(wins, losses) + tail = sum(math.comb(n, i) for i in range(k + 1)) / (2**n) + return wins, losses, min(1.0, 2 * tail) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("first") + ap.add_argument("second") + ap.add_argument("--set", dest="query_set", default=None, + help="docstring or paraphrase; both if omitted") + args = ap.parse_args() + + path = os.path.join(RESULTS, "fusion_check.json") + with open(path, encoding="utf-8") as fp: + report = json.load(fp) + + for name in (args.first, args.second): + if name not in report: + sys.exit(f"{name!r} not in {path}. Available: {', '.join(report)}") + + sets = [args.query_set] if args.query_set else ["docstring", "paraphrase"] + rng = random.Random(SEED) + + print(f"{args.first} vs {args.second}\n") + header = f"{'set':12} {'MRR a':>7} {'MRR b':>7} {'diff':>8} {'95% interval':>22} {'sign p':>8}" + print(header) + print("-" * len(header)) + + for name in sets: + a = report[args.first][name]["ranks"] + b = report[args.second][name]["ranks"] + if len(a) != len(b): + sys.exit(f"{name}: rank vectors differ in length, {len(a)} vs {len(b)}") + lo, hi = bootstrap(a, b, BOOTSTRAP, rng) + wins, losses, p = sign_test(a, b) + verdict = "" if lo <= 0 <= hi else " <- excludes zero" + print(f"{name:12} {mrr(a):>7.3f} {mrr(b):>7.3f} {mrr(a) - mrr(b):>8.3f} " + f"{f'[{lo:+.3f}, {hi:+.3f}]':>22} {p:>8.3f}{verdict}") + print(f"{'':12} {wins} queries better, {losses} worse, " + f"{len(a) - wins - losses} identical") + + print(f"\npaired bootstrap, {BOOTSTRAP} resamples, seed {SEED}; " + "sign test is two-sided and exact") + + +if __name__ == "__main__": + main() diff --git a/bench/truncation.py b/bench/truncation.py new file mode 100644 index 0000000..2c9d9fb --- /dev/null +++ b/bench/truncation.py @@ -0,0 +1,197 @@ +"""Does Qdrant Cloud Inference's 512-token window hurt all-MiniLM-L6-v2 on code? + + python bench/truncation.py + +Background. The bake-off scored the in-cluster MiniLM at 0.863 docstring +recall@10. An earlier offline run of the same model, over the same 5,000 +documents and the same 300 queries, scored 0.933. Same weights, same corpus, +seven points apart, which is too large to shrug at. + +Probing the service with progressively longer input shows it truncates at +**512 tokens**. The published `sentence-transformers` configuration for this +model sets `max_seq_length` to **256**: it was trained at that length, and +positions past it are ones it barely saw. A third of this corpus is longer than +that, so the two runs were not feeding the model the same thing. + +This turns the hypothesis into a measurement. It indexes the same corpus again +with every document cut down before it is sent, embeds it with the same +in-cluster MiniLM, and scores it against the same queries - on its own and in +the hybrid shape the demo ships. If the score climbs back toward the offline +figure, the window is the cause, and the fix for anyone using MiniLM through +Qdrant Cloud Inference is to cut their own input first. + +Writes one temporary collection and drops it. +""" + +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(__file__)) + +from common import DENSE, SPARSE, call, percentile, tokens_by_model # noqa: E402 + +COLLECTION = "bench-code-trunc" +EVAL_DIR = os.environ.get("EVAL_DIR", r"C:\Users\Home Laptop\code-search-eval") +RESULTS = os.path.join(os.path.dirname(__file__), "results") + +MODEL, DIMS = DENSE["minilm"] +TOKEN_BUDGET = 256 +# The service reports tokens only after the fact, so the cut is made locally and +# verified against what comes back. A first attempt cut at 150 words assuming +# ~1.6 tokens per word, and the verification caught it: Rust tokenizes at closer +# to 3.4 tokens per whitespace-separated word once punctuation and split +# identifiers are counted, so those documents were still hitting the 512 cap. +# Characters are the steadier proxy, at roughly 2.9 per token on this corpus. +CHAR_BUDGET = 700 +BATCH = 16 +PREFETCH = 100 +LIMIT = 10 +KS = (1, 5, 10) + + +def load(name): + with open(os.path.join(EVAL_DIR, name), encoding="utf-8") as fp: + return [json.loads(line) for line in fp if line.strip()] + + +def truncate(text): + return text[:CHAR_BUDGET] + + +def query_body(leg, text): + if leg == "dense": + return {"query": {"text": text, "model": MODEL}, "using": "dense", + "limit": LIMIT, "with_payload": ["doc_id"]} + return { + "prefetch": [ + {"query": {"text": text, "model": MODEL}, "using": "dense", "limit": PREFETCH}, + {"query": {"text": text, "model": SPARSE["bm25"]}, "using": "sparse", + "limit": PREFETCH}, + ], + "query": {"fusion": "rrf"}, + "limit": LIMIT, + "with_payload": ["doc_id"], + } + + +def main(): + corpus = load("corpus.jsonl") + sets = {"docstring": load("queries.jsonl"), "paraphrase": load("queries_paraphrase.jsonl")} + + cut = sum(1 for doc in corpus if len(doc["text"]) > CHAR_BUDGET) + print(f"{len(corpus)} documents, {cut} ({cut / len(corpus):.1%}) shortened to " + f"{CHAR_BUDGET} characters") + + # A sparse leg as well, so the truncated dense model can be scored in the + # hybrid shape the demo actually ships, not only on its own. BM25 reads the + # same truncated text: whatever the dense model was not shown, the lexical + # leg was not shown either, or the two are ranking different corpora. + call("DELETE", f"/collections/{COLLECTION}") + call("PUT", f"/collections/{COLLECTION}", { + "vectors": {"dense": {"size": DIMS, "distance": "Cosine"}}, + "sparse_vectors": {"sparse": {"modifier": "idf"}}, + }) + + max_tokens, total = 0, 0 + for start in range(0, len(corpus), BATCH): + points = [] + for i, doc in enumerate(corpus[start : start + BATCH], start=start): + text = truncate(doc["text"]) + points.append({ + "id": i, + "vector": { + "dense": {"text": text, "model": MODEL}, + "sparse": {"text": text, "model": SPARSE["bm25"]}, + }, + "payload": {"doc_id": doc["id"]}, + }) + _r, _ms, usage = call("PUT", f"/collections/{COLLECTION}/points?wait=true", + {"points": points}) + used = sum(tokens_by_model(usage).values()) + total += used + max_tokens = max(max_tokens, used / max(len(points), 1)) + if (start + BATCH) % 1600 == 0: + print(f" indexed {min(start + BATCH, len(corpus))}/{len(corpus)}", flush=True) + + mean_tokens = total / len(corpus) + under_budget = max_tokens <= TOKEN_BUDGET + print(f" mean {mean_tokens:.0f} tokens/doc, worst batch mean {max_tokens:.0f} " + f"(budget {TOKEN_BUDGET})") + if not under_budget: + # Reported rather than silently accepted. The cut is per-document and the + # check is per-batch, so a batch of unusually dense code can still average + # over; the result below is still a cut-input measurement, just not a + # strictly-under-256 one. + print(" note: some batches averaged over the budget; lower CHAR_BUDGET to tighten") + + report = { + "char_budget": CHAR_BUDGET, + "documents_shortened": cut, + "mean_tokens_per_doc": round(mean_tokens, 1), + "worst_batch_mean_tokens": round(max_tokens, 1), + "under_budget": under_budget, + "sets": {}, + } + + for leg in ("dense", "hybrid"): + for set_name, queries in sets.items(): + ranks, times = [], [] + for q in queries: + result, ms, _u = call("POST", f"/collections/{COLLECTION}/points/query", + query_body(leg, q["query"])) + rank = next((r for r, point in enumerate(result["points"], 1) + if point["payload"]["doc_id"] == q["answer_id"]), None) + ranks.append(rank) + times.append(ms) + n = len(queries) + key = f"{leg}/{set_name}" + report["sets"][key] = { + **{f"recall@{k}": round(sum(1 for r in ranks if r and r <= k) / n, 4) + for k in KS}, + "mrr@10": round(sum(1.0 / r for r in ranks if r and r <= 10) / n, 4), + "p50_ms": percentile(times, 50), + } + print(f" {key}: {json.dumps(report['sets'][key])}", flush=True) + + call("DELETE", f"/collections/{COLLECTION}") + + os.makedirs(RESULTS, exist_ok=True) + out = os.path.join(RESULTS, "truncation.json") + with open(out, "w", encoding="utf-8") as fp: + json.dump(report, fp, indent=2) + + # The full-text rows, so the comparison reads without a second file open. + baseline = {} + try: + with open(os.path.join(RESULTS, "bakeoff.json"), encoding="utf-8") as fp: + for row in json.load(fp)["rows"]: + if row["config"] in ("minilm only", "minilm + bm25 (RRF)"): + baseline[row["config"]] = row + except FileNotFoundError: + pass + + header = f"{'configuration':32} {'docR@10':>8} {'docMRR':>8} {'parR@10':>8} {'parMRR':>8}" + print("\n" + header) + print("-" * len(header)) + + def row(name, doc, par): + print(f"{name:32} {doc['recall@10']:>8.3f} {doc['mrr@10']:>8.3f} " + f"{par['recall@10']:>8.3f} {par['mrr@10']:>8.3f}") + + if "minilm only" in baseline: + b = baseline["minilm only"] + row("minilm, full text", b["docstring"], b["paraphrase"]) + row("minilm, cut input", report["sets"]["dense/docstring"], + report["sets"]["dense/paraphrase"]) + if "minilm + bm25 (RRF)" in baseline: + b = baseline["minilm + bm25 (RRF)"] + row("minilm + bm25, full text", b["docstring"], b["paraphrase"]) + row("minilm + bm25, cut input", report["sets"]["hybrid/docstring"], + report["sets"]["hybrid/paraphrase"]) + + print(f"\nwritten to {out}") + + +if __name__ == "__main__": + main() diff --git a/code_search/__init__.py b/code_search/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/code_search/config.py b/code_search/config.py deleted file mode 100644 index b26e6c0..0000000 --- a/code_search/config.py +++ /dev/null @@ -1,83 +0,0 @@ -import os -from urllib.parse import urlparse - -from dotenv import load_dotenv -from qdrant_client import QdrantClient - -load_dotenv() - -CODE_DIR = os.path.dirname(__file__) -ROOT_DIR = os.path.dirname(CODE_DIR) -DATA_DIR = os.path.join(ROOT_DIR, "data") - -QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333") - -def _key_problem(value: str | None) -> str | None: - """Describe why a key is unusable, or None when it looks fine. - - Non-ASCII keys are worth catching here: httpx raises UnicodeEncodeError - deep inside header construction, which surfaces as an unrelated-looking - stack trace. "" is what some PaaS dashboards store when a - variable was set from an unresolved reference. - """ - if not value: - return "QDRANT_API_KEY is not set" - if value == "": - return "QDRANT_API_KEY is the literal string '', so the variable never resolved" - if len(value) < 20: - return f"QDRANT_API_KEY is only {len(value)} characters, which is too short to be a key" - try: - value.encode("ascii") - except UnicodeEncodeError: - return "QDRANT_API_KEY contains non-ASCII characters, usually a copy-paste artifact" - return None - - -QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY") - -# Fail at import with the actual reason rather than carrying on with a bad key. -# A silent fallback here hides a broken deploy variable and turns a one-line -# config error into an afternoon of debugging downstream symptoms. -if QDRANT_URL.startswith("https://"): - _problem = _key_problem(QDRANT_API_KEY) - if _problem: - raise RuntimeError( - f"{_problem}. A remote QDRANT_URL ({QDRANT_URL}) needs a valid API key. " - "Set QDRANT_API_KEY in the environment." - ) - -QDRANT_CODE_COLLECTION_NAME = "code-snippets-unixcoder" -QDRANT_NLU_COLLECTION_NAME = "code-signatures" -QDRANT_FILE_COLLECTION_NAME = "code-files" - -ENCODER_NAME = "all-MiniLM-L6-v2" -ENCODER_SIZE = 384 - -# Commit of qdrant/qdrant the collections were built from. Result links carry -# line numbers, and resolving them against a moving `master` quietly points at -# the wrong lines as the source changes. Set this to the SHA the indexing run -# reports; `master` is the old behaviour and stays the default so an unset -# variable degrades to what it did before rather than breaking links. -INDEXED_COMMIT = os.environ.get("INDEXED_COMMIT", "master") - - -def make_qdrant_client() -> QdrantClient: - """Construct a QdrantClient from QDRANT_URL. - - Explicit host/port/https params (instead of just `url=`) so we don't get - caught by qdrant-client's URL parsing quirks — some Railway-like PaaS - egress environments seem to fail on the client's default port assumptions, - surfacing as "[Errno 111] Connection refused" even when the URL is - reachable via curl. - """ - parsed = urlparse(QDRANT_URL) - https = parsed.scheme == "https" - port = parsed.port or (443 if https else 6333) - return QdrantClient( - host=parsed.hostname, - port=port, - https=https, - api_key=QDRANT_API_KEY, - prefer_grpc=False, - timeout=60, - ) diff --git a/code_search/get_file.py b/code_search/get_file.py deleted file mode 100644 index d40ed9e..0000000 --- a/code_search/get_file.py +++ /dev/null @@ -1,34 +0,0 @@ -from qdrant_client.http import models - -from code_search.config import QDRANT_FILE_COLLECTION_NAME, make_qdrant_client - - -class FileGet: - - def __init__(self): - self.collection_name = QDRANT_FILE_COLLECTION_NAME - self.client = make_qdrant_client() - - def get(self, path, limit=5) -> list[dict]: - points, _next_offset = self.client.scroll( - collection_name=self.collection_name, - scroll_filter=models.Filter( - must=[ - models.FieldCondition( - key="path", - match=models.MatchValue(value=path), - ) - ] - ), - limit=limit, - ) - - return [point.payload for point in points] - - -if __name__ == '__main__': - searcher = FileGet() - - res = searcher.get("lib/collection/src/collection_manager/optimizers/indexing_optimizer.rs") - for hit in res: - print(hit) diff --git a/code_search/index/__init__.py b/code_search/index/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/code_search/index/file_uploader.py b/code_search/index/file_uploader.py deleted file mode 100644 index 159c320..0000000 --- a/code_search/index/file_uploader.py +++ /dev/null @@ -1,57 +0,0 @@ -from pathlib import Path -from qdrant_client import QdrantClient -import json - -from code_search.config import QDRANT_URL, QDRANT_API_KEY, DATA_DIR, QDRANT_FILE_COLLECTION_NAME - - -def encode_and_upload(): - qdrant_client = QdrantClient( - QDRANT_URL, - api_key=QDRANT_API_KEY, - ) - - collection_name = QDRANT_FILE_COLLECTION_NAME - input_file = Path(DATA_DIR) / "rs_files.json" - - if not input_file.exists(): - raise RuntimeError(f"File {input_file} does not exist. Skipping") - - payload = [] - with open(input_file, 'r') as json_file: - data = json.load(json_file) - payload = data - - print(f"Recreating the collection {collection_name}") - if qdrant_client.collection_exists(collection_name): - qdrant_client.delete_collection(collection_name) - qdrant_client.create_collection( - collection_name=collection_name, - vectors_config={} - ) - - print(f"Storing data in the collection {collection_name}") - qdrant_client.upload_collection( - collection_name=collection_name, - payload=payload, - vectors=[{}] * len(payload), - ids=None, - batch_size=256 - ) - - # /api/file is a filtered scroll on `path` and nothing else reads this - # collection. Clusters with strict mode on refuse to filter an unindexed - # field, so without this the file viewer fails with a 500 on every result - # anyone clicks - while search itself keeps working, which makes it look - # like a frontend problem. - print(f"Indexing `path` in the collection {collection_name}") - qdrant_client.create_payload_index( - collection_name=collection_name, - field_name="path", - field_schema="keyword", - wait=True, - ) - - -if __name__ == '__main__': - encode_and_upload() diff --git a/code_search/index/helper.py b/code_search/index/helper.py deleted file mode 100644 index 60b66fb..0000000 --- a/code_search/index/helper.py +++ /dev/null @@ -1,146 +0,0 @@ -from abc import abstractmethod -from typing import Union, List, Optional - -from sentence_transformers import SentenceTransformer -from transformers import AutoTokenizer, AutoModel - -import numpy as np -import torch -import re - -from code_search.model.unixcoder import UniXcoder - - -class BaseEmbeddingsProvider: - - @abstractmethod - def embed_code( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> np.array: - """Converts code and/or docstring to vector""" - - -class AutoModelEmbeddingsProvider(BaseEmbeddingsProvider): - def __init__( - self, - model_name: str = "microsoft/codebert-base", - tokenizer_name: Optional[str] = None, - max_tokens: int = 512, - ): - if tokenizer_name is None: - tokenizer_name = model_name - self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_name) - self.model = AutoModel.from_pretrained(model_name) - self.max_tokens = max_tokens - self.model_name = model_name - - def embed_code( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> np.array: - token_ids = self.get_token_ids(code, docstring)[:512] - context_embeddings = ( - self.model(torch.tensor(token_ids)[None, :])[0].squeeze().detach().numpy() - ) - if 1 == len(context_embeddings.shape): - return context_embeddings - return np.mean(context_embeddings, axis=0) - - def get_token_ids( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> Union[int, List[int]]: - tokens = self.get_tokens(code, docstring) - tokens_ids = self.tokenizer.convert_tokens_to_ids(tokens) - return tokens_ids - - def get_tokens( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> List[str]: - # Maximum number of tokens has to include the separators used by CodeBERT - max_tokens = self.max_tokens - 3 - - code_tokens = [] - if code is not None: - code_tokens = self.tokenizer.tokenize( - code, max_length=max_tokens, truncation=True - ) - docstring_tokens = [] - if docstring is not None: - docstring_tokens = self.tokenizer.tokenize( - docstring, max_length=max_tokens, truncation=True - ) - - # If both code and docstring is provided, we need to cut off some - # tokens above the limit. Here there preference is to remove the code - # as it should be longer in general. - n_code_tokens, n_doc_tokens = ( - min(len(code_tokens), max_tokens), - min(len(docstring_tokens), max_tokens), - ) - n_code_tokens -= n_doc_tokens - - # Build all the tokens using some possible separators. The separators - # are aligned to CodeBERT model, but if a selected transformer does not - # have them, everything should also work. - tokens = [] - if hasattr(self.tokenizer, "cls_token"): - tokens.append(self.tokenizer.cls_token) - tokens.extend(code_tokens[:n_code_tokens]) - if hasattr(self.tokenizer, "sep_token"): - tokens.append(self.tokenizer.sep_token) - tokens.extend(docstring_tokens[:n_doc_tokens]) - if hasattr(self.tokenizer, "eos_token"): - tokens.append(self.tokenizer.eos_token) - return tokens - - def __str__(self): - return self.model_name - - -class UniXcoderEmbeddingsProvider(BaseEmbeddingsProvider): - def __init__(self, device: Optional[str] = None): - default_device = "cuda" if torch.cuda.is_available() else "cpu" - self.device = torch.device(default_device if device is None else device) - self.model = UniXcoder("microsoft/unixcoder-base") - self.model.to(self.device) - self.model_name = "microsoft/unixcoder-base" - - def embed_code( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> np.array: - tokens_ids = self.model.tokenize( - [f"{docstring or ''} {code or ''}"], max_length=512, mode="" - ) - source_ids = torch.tensor(tokens_ids).to(self.device) - _, func_embedding = self.model(source_ids) - vector = func_embedding.detach().cpu().numpy()[0] - return vector - - -class SentenceTransformerEmbeddingsProvider(BaseEmbeddingsProvider): - camel_case_regex = re.compile(r"([a-z\s])([A-Z])") - underscore_regex = re.compile(r"([a-z])_([a-z])") - special_chars_regex = re.compile(r"\(|\)|\{|\}|\<|\>|\[|\]|\&|::|;") - method_call_regex = re.compile(r"\.([a-z])") - multiple_white_char_regex = re.compile(r"\s{2,}") - - def __init__(self, sentence_transformer_name: str): - self.model = SentenceTransformer(sentence_transformer_name) - self.model_name = sentence_transformer_name - - def embed_code( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> np.array: - inputs = [] - if docstring is not None: - inputs.append(docstring) - if code is not None: - inputs.append(code) - return self.model.encode(self._preprocess_text(" ".join(inputs))) - - def _preprocess_text(self, text: str) -> str: - text = self.camel_case_regex.sub("\\1 \\2", text) - text = self.underscore_regex.sub("\\1 \\2", text) - text = self.special_chars_regex.sub(" ", text) - text = self.method_call_regex.sub(" \\1", text) - text = self.multiple_white_char_regex.sub(" ", text) - return text diff --git a/code_search/index/upload_code.py b/code_search/index/upload_code.py deleted file mode 100644 index 78946e9..0000000 --- a/code_search/index/upload_code.py +++ /dev/null @@ -1,100 +0,0 @@ -from pathlib import Path -from tqdm import tqdm -from qdrant_client.http import models as rest - -import qdrant_client -import numpy as np -import json - -from code_search.config import QDRANT_URL, QDRANT_API_KEY, DATA_DIR, QDRANT_CODE_COLLECTION_NAME -from code_search.model.encoder import UniXcoderEmbeddingsProvider - -code_keys = [ - "code_snippet", - "body", - "signature", - "name", -] - - -def encode_and_upload(): - client = qdrant_client.QdrantClient( - QDRANT_URL, - api_key=QDRANT_API_KEY, - prefer_grpc=True, - ) - - collection_name = QDRANT_CODE_COLLECTION_NAME - input_file = Path(DATA_DIR) / "qdrant_snippets.jsonl" - encoder = UniXcoderEmbeddingsProvider() - - input_file = Path(DATA_DIR) / input_file - output_file = Path(DATA_DIR) / f"{collection_name}.npy" - - if not input_file.exists(): - raise RuntimeError(f"File {input_file} does not exist. Skipping") - - if output_file.exists(): - print(f"File {output_file} already exists. Skipping encoding.") - embeddings = np.load(str(output_file)).tolist() - else: - print(f"Preparing the output for {output_file}") - - embeddings = [] - with open(input_file, "r", encoding="utf-8") as fp: - for line in tqdm(fp): - line_dict = json.loads(line) - - body = None - for code_key in code_keys: - body = line_dict.get(code_key) - if body is not None: - break - docstring = line_dict.get("docstring") - - if body is None or len(body) == 0: - continue - - embedding = encoder.embed_code(body, docstring) - embeddings.append(embedding) - - np.save(str(output_file), np.array(embeddings)) - - payloads = [] - with open(input_file, "r", encoding="utf-8") as fp: - for line in tqdm(fp): - line_dict = json.loads(line) - payloads.append(line_dict) - - print(f"Embeddings shape: ({len(embeddings)}, {len(embeddings[0])})") - - print(f"Recreating the collection {collection_name}") - if client.collection_exists(collection_name): - client.delete_collection(collection_name) - client.create_collection( - collection_name=collection_name, - vectors_config=rest.VectorParams( - size=len(embeddings[1]), - distance=rest.Distance.COSINE, - on_disk=True, - ), - quantization_config=rest.ScalarQuantization( - scalar=rest.ScalarQuantizationConfig( - type=rest.ScalarType.INT8, - always_ram=True, - quantile=0.99, - ) - ) - ) - - print(f"Storing data in the collection {collection_name}") - client.upload_collection( - collection_name=collection_name, - ids=[i for i, _ in enumerate(embeddings)], - vectors=embeddings, - payload=payloads, - ) - - -if __name__ == '__main__': - encode_and_upload() diff --git a/code_search/index/upload_signatures.py b/code_search/index/upload_signatures.py deleted file mode 100644 index 732ae23..0000000 --- a/code_search/index/upload_signatures.py +++ /dev/null @@ -1,78 +0,0 @@ -import json -from pathlib import Path - -import tqdm -from qdrant_client import QdrantClient, models -from qdrant_client.models import Distance, VectorParams -from sentence_transformers import SentenceTransformer - -from code_search.config import DATA_DIR, QDRANT_URL, QDRANT_API_KEY, QDRANT_NLU_COLLECTION_NAME, ENCODER_NAME, \ - ENCODER_SIZE -from code_search.index.textifier import textify - -file_name = Path(DATA_DIR) / "structures.json" - - -def iter_batch(iterable, batch_size=64): - batch = [] - for item in iterable: - batch.append(item) - if len(batch) == batch_size: - yield batch - batch = [] - if batch: - yield batch - - -def load_records(): - with open(file_name, "r", encoding="utf-8") as fp: - for line in fp: - row = json.loads(line) - yield row - - -def encode(sentence_transformer_name=ENCODER_NAME): - model = SentenceTransformer(sentence_transformer_name) - for batch in iter_batch(load_records()): - texts = [textify(row) for row in batch] - embeddings = model.encode(texts).tolist() - yield from embeddings - - -def upload(): - collection_name = QDRANT_NLU_COLLECTION_NAME - - client = QdrantClient( - QDRANT_URL, - api_key=QDRANT_API_KEY, - prefer_grpc=True, - ) - - print(f"Recreating the collection {collection_name}") - if client.collection_exists(collection_name): - client.delete_collection(collection_name) - client.create_collection( - collection_name=collection_name, - vectors_config=VectorParams( - size=ENCODER_SIZE, - distance=Distance.COSINE, - on_disk=True, - ), - quantization_config=models.ScalarQuantization( - scalar=models.ScalarQuantizationConfig( - type=models.ScalarType.INT8, - always_ram=True, - quantile=0.99, - ) - ) - ) - - client.upload_collection( - collection_name=collection_name, - vectors=encode(), - payload=tqdm.tqdm(load_records()), - ) - - -if __name__ == '__main__': - upload() diff --git a/code_search/model/__init__.py b/code_search/model/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/code_search/model/encoder.py b/code_search/model/encoder.py deleted file mode 100644 index cf59929..0000000 --- a/code_search/model/encoder.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Optional, List -from .unixcoder import UniXcoder - -import torch - - -class UniXcoderEmbeddingsProvider: - def __init__(self, device: Optional[str] = None): - default_device = "cuda" if torch.cuda.is_available() else "cpu" - self.device = torch.device(default_device if device is None else device) - self.model = UniXcoder("microsoft/unixcoder-base") - self.model.to(self.device) - self.model_name = "microsoft/unixcoder-base" - - def embed_code( - self, code: Optional[str] = None, docstring: Optional[str] = None - ) -> List[float]: - tokens_ids = self.model.tokenize( - [f"{docstring or ''} {code or ''}"], max_length=512, mode="" - ) - source_ids = torch.tensor(tokens_ids).to(self.device) - # Autograd was live for every query. Nothing here is ever backpropagated, - # so the graph was built and immediately discarded. Qdrant answers a - # search in well under a millisecond while this pass runs into hundreds, - # which makes it the only part of a query worth shaving. - with torch.inference_mode(): - _, func_embedding = self.model(source_ids) - vector = func_embedding.cpu().numpy()[0] - return vector.tolist() diff --git a/code_search/model/unixcoder.py b/code_search/model/unixcoder.py deleted file mode 100644 index f8c6901..0000000 --- a/code_search/model/unixcoder.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT license. - -import torch -import torch.nn as nn -from transformers import RobertaTokenizer, RobertaModel, RobertaConfig - - -class UniXcoder(nn.Module): - def __init__(self, model_name): - """ - Build UniXcoder. - - Parameters: - - * `model_name`- huggingface model card name. e.g. microsoft/unixcoder-base - """ - super(UniXcoder, self).__init__() - self.tokenizer = RobertaTokenizer.from_pretrained(model_name) - self.config = RobertaConfig.from_pretrained(model_name) - self.config.is_decoder = True - self.model = RobertaModel.from_pretrained(model_name, config=self.config) - - self.register_buffer( - "bias", - torch.tril(torch.ones((1024, 1024), dtype=torch.uint8)).view(1, 1024, 1024), - ) - self.lm_head = nn.Linear( - self.config.hidden_size, self.config.vocab_size, bias=False - ) - self.lm_head.weight = self.model.embeddings.word_embeddings.weight - self.lsm = nn.LogSoftmax(dim=-1) - - self.tokenizer.add_tokens([""], special_tokens=True) - - def tokenize(self, inputs, mode="", max_length=512, padding=False): - """ - Convert string to token ids - - Parameters: - - * `inputs`- list of input strings. - * `max_length`- The maximum total source sequence length after tokenization. - * `padding`- whether to pad source sequence length to max_length. - * `mode`- which mode the sequence will use. i.e. , , - """ - assert mode in ["", "", ""] - assert max_length < 1024 - - tokenizer = self.tokenizer - - tokens_ids = [] - for x in inputs: - tokens = tokenizer.tokenize(x) - if mode == "": - tokens = tokens[: max_length - 4] - tokens = ( - [tokenizer.cls_token, mode, tokenizer.sep_token] - + tokens - + [tokenizer.sep_token] - ) - elif mode == "": - tokens = tokens[-(max_length - 3) :] - tokens = [tokenizer.cls_token, mode, tokenizer.sep_token] + tokens - else: - tokens = tokens[: max_length - 5] - tokens = ( - [tokenizer.cls_token, mode, tokenizer.sep_token] - + tokens - + [tokenizer.sep_token] - ) - - tokens_id = tokenizer.convert_tokens_to_ids(tokens) - if padding: - tokens_id = tokens_id + [self.config.pad_token_id] * ( - max_length - len(tokens_id) - ) - tokens_ids.append(tokens_id) - return tokens_ids - - def decode(self, source_ids): - """Convert token ids to string""" - predictions = [] - for x in source_ids: - prediction = [] - for y in x: - t = y.cpu().numpy() - t = list(t) - if 0 in t: - t = t[: t.index(0)] - text = self.tokenizer.decode(t, clean_up_tokenization_spaces=False) - prediction.append(text) - predictions.append(prediction) - return predictions - - def forward(self, source_ids): - """Obtain token embeddings and sentence embeddings""" - mask = source_ids.ne(self.config.pad_token_id) - token_embeddings = self.model( - source_ids, attention_mask=mask.unsqueeze(1) * mask.unsqueeze(2) - )[0] - sentence_embeddings = (token_embeddings * mask.unsqueeze(-1)).sum(1) / mask.sum( - -1 - ).unsqueeze(-1) - return token_embeddings, sentence_embeddings - - def generate( - self, source_ids, decoder_only=True, eos_id=None, beam_size=5, max_length=64 - ): - """Generate sequence given context (source_ids)""" - - # Set encoder mask attention matrix: bidirectional for , unirectional for - if decoder_only: - mask = self.bias[:, : source_ids.size(-1), : source_ids.size(-1)] - else: - mask = source_ids.ne(self.config.pad_token_id) - mask = mask.unsqueeze(1) * mask.unsqueeze(2) - - if eos_id is None: - eos_id = self.config.eos_token_id - - device = source_ids.device - - # Decoding using beam search - preds = [] - zero = torch.LongTensor(1).fill_(0).to(device) - source_len = list(source_ids.ne(1).sum(-1).cpu().numpy()) - length = source_ids.size(-1) - encoder_output = self.model(source_ids, attention_mask=mask) - for i in range(source_ids.shape[0]): - context = [ - [x[i : i + 1, :, : source_len[i]].repeat(beam_size, 1, 1, 1) for x in y] - for y in encoder_output.past_key_values - ] - beam = Beam(beam_size, eos_id, device) - input_ids = beam.getCurrentState().clone() - context_ids = source_ids[i : i + 1, : source_len[i]].repeat(beam_size, 1) - out = encoder_output.last_hidden_state[i : i + 1, : source_len[i]].repeat( - beam_size, 1, 1 - ) - for _ in range(max_length): - if beam.done(): - break - if _ == 0: - hidden_states = out[:, -1, :] - out = self.lsm(self.lm_head(hidden_states)).data - beam.advance(out) - input_ids.data.copy_( - input_ids.data.index_select(0, beam.getCurrentOrigin()) - ) - input_ids = beam.getCurrentState().clone() - else: - length = context_ids.size(-1) + input_ids.size(-1) - out = self.model( - input_ids, - attention_mask=self.bias[ - :, context_ids.size(-1) : length, :length - ], - past_key_values=context, - ).last_hidden_state - hidden_states = out[:, -1, :] - out = self.lsm(self.lm_head(hidden_states)).data - beam.advance(out) - input_ids.data.copy_( - input_ids.data.index_select(0, beam.getCurrentOrigin()) - ) - input_ids = torch.cat( - (input_ids, beam.getCurrentState().clone()), -1 - ) - hyp = beam.getHyp(beam.getFinal()) - pred = beam.buildTargetTokens(hyp)[:beam_size] - pred = [ - torch.cat( - [x.view(-1) for x in p] + [zero] * (max_length - len(p)) - ).view(1, -1) - for p in pred - ] - preds.append(torch.cat(pred, 0).unsqueeze(0)) - - preds = torch.cat(preds, 0) - - return preds - - -class Beam(object): - def __init__(self, size, eos, device): - self.size = size - self.device = device - # The score for each translation on the beam. - self.scores = torch.FloatTensor(size).zero_().to(device) - # The backpointers at each time-step. - self.prevKs = [] - # The outputs at each time-step. - self.nextYs = [torch.LongTensor(size).fill_(0).to(device)] - # Has EOS topped the beam yet. - self._eos = eos - self.eosTop = False - # Time and k pair for finished. - self.finished = [] - - def getCurrentState(self): - "Get the outputs for the current timestep." - batch = self.nextYs[-1].view(-1, 1) - return batch - - def getCurrentOrigin(self): - "Get the backpointers for the current timestep." - return self.prevKs[-1] - - def advance(self, wordLk): - """ - Given prob over words for every last beam `wordLk` and attention - `attnOut`: Compute and update the beam search. - - Parameters: - - * `wordLk`- probs of advancing from the last step (K x words) - * `attnOut`- attention at the last step - - Returns: True if beam search is complete. - """ - numWords = wordLk.size(1) - - # Sum the previous scores. - if len(self.prevKs) > 0: - beamLk = wordLk + self.scores.unsqueeze(1).expand_as(wordLk) - - # Don't let EOS have children. - for i in range(self.nextYs[-1].size(0)): - if self.nextYs[-1][i] == self._eos: - beamLk[i] = -1e20 - else: - beamLk = wordLk[0] - flatBeamLk = beamLk.view(-1) - bestScores, bestScoresId = flatBeamLk.topk(self.size, 0, True, True) - - self.scores = bestScores - - # bestScoresId is flattened beam x word array, so calculate which - # word and beam each score came from - prevK = torch.div(bestScoresId, numWords, rounding_mode="floor") - self.prevKs.append(prevK) - self.nextYs.append((bestScoresId - prevK * numWords)) - - for i in range(self.nextYs[-1].size(0)): - if self.nextYs[-1][i] == self._eos: - s = self.scores[i] - self.finished.append((s, len(self.nextYs) - 1, i)) - - # End condition is when top-of-beam is EOS and no global score. - if self.nextYs[-1][0] == self._eos: - self.eosTop = True - - def done(self): - return self.eosTop and len(self.finished) >= self.size - - def getFinal(self): - if len(self.finished) == 0: - self.finished.append((self.scores[0], len(self.nextYs) - 1, 0)) - self.finished.sort(key=lambda a: -a[0]) - if len(self.finished) != self.size: - unfinished = [] - for i in range(self.nextYs[-1].size(0)): - if self.nextYs[-1][i] != self._eos: - s = self.scores[i] - unfinished.append((s, len(self.nextYs) - 1, i)) - unfinished.sort(key=lambda a: -a[0]) - self.finished += unfinished[: self.size - len(self.finished)] - return self.finished[: self.size] - - def getHyp(self, beam_res): - """ - Walk back to construct the full hypothesis. - """ - hyps = [] - for _, timestep, k in beam_res: - hyp = [] - for j in range(len(self.prevKs[:timestep]) - 1, -1, -1): - hyp.append(self.nextYs[j + 1][k]) - k = self.prevKs[j][k] - hyps.append(hyp[::-1]) - return hyps - - def buildTargetTokens(self, preds): - sentence = [] - for pred in preds: - tokens = [] - for tok in pred: - if tok == self._eos: - break - tokens.append(tok) - sentence.append(tokens) - return sentence diff --git a/code_search/postprocessing.py b/code_search/postprocessing.py deleted file mode 100644 index 45dfc6b..0000000 --- a/code_search/postprocessing.py +++ /dev/null @@ -1,97 +0,0 @@ -from collections import defaultdict -from typing import List - - -def merge_search_results(code_search_result: List[dict], nlu_search_result: List[dict]) -> List[dict]: - """Merge search results from code and NLU searchers - - Args: - code_search_result (List[dict]): Code search results - Examples: - [ - {"end_line": 127, "file": "lib/segment/src/index/query_estimator.rs", "start_line": 123} - {"end_line": 830, "file": "lib/segment/src/segment.rs", "start_line": 827} - {"end_line": 169, "file": "lib/segment/src/index/field_index/field_index_base.rs", "start_line": 166} - {"end_line": 162, "file": "lib/segment/src/index/query_estimator.rs", "start_line": 158} - {"end_line": 152, "file": "lib/collection/src/shards/local_shard_operations.rs", "start_line": 150} - ] - nlu_search_result (List[dict]): NLU search results - Examples: - [ - { - "code_type": "Function", - "context": { - "file_name": "query_estimator.rs", - "file_path": "lib/segment/src/index/query_estimator.rs", - "module": "index", - "snippet": "...", - "struct_name": null - }, - "docstring": null, - "line": 13, - "line_from": 13, - "line_to": 39, - "name": "combine_should_estimations", - "signature": "fn combine_should_estimations () -> CardinalityEstimation" - } - ] - """ - - code_search_result_by_file = defaultdict(list) - for hit in code_search_result: - code_search_result_by_file[hit["file"]].append(hit) - - for nlu_search_hit in nlu_search_result: - file = nlu_search_hit["context"]["file_path"] - if file in code_search_result_by_file: - nlu_search_hit["sub_matches"] = try_merge_overlapping_snippets( - code_search_result_by_file[file], - nlu_search_hit - ) - nlu_search_result = sorted(nlu_search_result, key=lambda x: -len(x.get('sub_matches', []))) - - return nlu_search_result - - - -def try_merge_overlapping_snippets(code_search_results: List[dict], nlu_search_result: dict) -> List[dict]: - """Find code search results that overlap with NLU search results and merge them - Use nlu_search_result as a base for merging - - Args: - code_search_results: - [ - {"end_line": 127, "start_line": 123, ...} - {"end_line": 830, "start_line": 827, ...} - {"end_line": 169, "start_line": 166, ...} - {"end_line": 162, "start_line": 158, ...} - {"end_line": 14, "start_line": 16, ...} - ] - nlu_search_result: - { - "line": 13, - "line_from": 13, - "line_to": 39, - ... - } - - Returns: Overlapping code search results merged with NLU search results - """ - overlapped = [] - code_search_result = sorted(code_search_results, key=lambda x: x["start_line"]) - for code_search_hit in code_search_result: - from_a = code_search_hit["start_line"] + 1 - to_a = code_search_hit["end_line"] + 1 - from_b = nlu_search_result["line_from"] - to_b = nlu_search_result["line_to"] - - # get overlapping range - start = max(from_a, from_b) - end = min(to_a, to_b) - if start <= end: - overlapped.append({ - "overlap_from": start, - "overlap_to": end, - }) - - return overlapped diff --git a/code_search/searcher.py b/code_search/searcher.py deleted file mode 100644 index cf72734..0000000 --- a/code_search/searcher.py +++ /dev/null @@ -1,86 +0,0 @@ -import json -from concurrent.futures import ThreadPoolExecutor -from functools import lru_cache - -from sentence_transformers import SentenceTransformer - -from code_search.config import ENCODER_NAME, QDRANT_CODE_COLLECTION_NAME, \ - QDRANT_NLU_COLLECTION_NAME, make_qdrant_client -from code_search.model.encoder import UniXcoderEmbeddingsProvider -from code_search.postprocessing import merge_search_results - - -class CodeSearcher: - - def __init__(self): - self.collection_name = QDRANT_CODE_COLLECTION_NAME - self.client = make_qdrant_client() - self.encoder = UniXcoderEmbeddingsProvider("cpu") - # The vector depends only on the query text, and encoding is where - # essentially all of a search's time goes. The demo ships example - # queries that get clicked far more than anything else, so caching makes - # the common path free. Bounded, so it cannot grow without limit. - self._embed = lru_cache(maxsize=512)(self._embed_query) - - def _embed_query(self, query: str) -> tuple: - return tuple(self.encoder.embed_code(docstring=query)) - - def search(self, query, limit=5) -> list[dict]: - vector = list(self._embed(query)) - result = self.client.query_points( - collection_name=self.collection_name, - query=vector, - limit=limit, - with_payload=["start_line", "end_line", "file"], - ) - - return [hit.payload for hit in result.points] - - -class NluSearcher: - - def __init__(self): - self.collection_name = QDRANT_NLU_COLLECTION_NAME - self.client = make_qdrant_client() - self.encoder = SentenceTransformer(ENCODER_NAME) - self._embed = lru_cache(maxsize=512)(self._embed_query) - - def _embed_query(self, query: str) -> tuple: - return tuple(self.encoder.encode([query])[0].tolist()) - - def search(self, query, limit=5) -> list[dict]: - vector = list(self._embed(query)) - result = self.client.query_points( - collection_name=self.collection_name, - query=vector, - limit=limit, - ) - - return [hit.payload for hit in result.points] - - -class CombinedSearcher: - - def __init__(self): - self.nlu_searcher = NluSearcher() - self.code_searcher = CodeSearcher() - # The two searches ran one after the other, so a query paid for both - # forward passes in series. They share nothing: separate models, - # separate Qdrant clients. Torch releases the GIL during inference, so - # given more than one core these genuinely overlap, and on a single core - # it costs no more than the pool itself. - self._pool = ThreadPoolExecutor(max_workers=2, thread_name_prefix="search") - - def search(self, query, limit=5, code_limit=20) -> list[dict]: - nlu_future = self._pool.submit(self.nlu_searcher.search, query, limit) - code_future = self._pool.submit(self.code_searcher.search, query, code_limit) - - return merge_search_results(code_future.result(), nlu_future.result()) - - -if __name__ == '__main__': - searcher = CombinedSearcher() - - res = searcher.search("cardinality of should request") - for hit in res: - print(json.dumps(hit)) diff --git a/code_search/service.py b/code_search/service.py deleted file mode 100644 index 58f76ad..0000000 --- a/code_search/service.py +++ /dev/null @@ -1,171 +0,0 @@ -import json -import os -import time -from pathlib import Path - -from fastapi import FastAPI, HTTPException -from fastapi.middleware.cors import CORSMiddleware -from starlette.staticfiles import StaticFiles - -from code_search.config import DATA_DIR, INDEXED_COMMIT, ROOT_DIR -from code_search.get_file import FileGet -from code_search.searcher import CombinedSearcher - -app = FastAPI() - -# CORS_ORIGINS is a comma-separated allowlist of frontend origins allowed to -# call this API. Use "*" only when the backend is public and stateless. -# Example: "https://code-search.vercel.app,https://staging.example.com" -cors_origins = [ - o.strip() - for o in os.environ.get("CORS_ORIGINS", "").split(",") - if o.strip() -] -if cors_origins: - # No credentials: this API has no cookies or auth, and pairing - # `allow_credentials=True` with an "*" origin is rejected by browsers - # anyway - the combination is invalid, so the permissive setup it was - # meant to enable is the one it would have broken. - app.add_middleware( - CORSMiddleware, - allow_origins=cors_origins, - allow_credentials=False, - allow_methods=["GET"], - allow_headers=["*"], - ) - -searcher = CombinedSearcher() -get_file = FileGet() - - -def _load_fallback_index() -> list[dict]: - """Load rust-parser structures for keyword fallback. - - Used only while the unixcoder embeddings collection is still building. - Returns [] if the file isn't there, which disables the fallback. - """ - path = Path(DATA_DIR) / "structures.json" - if not path.exists(): - return [] - records = [] - with open(path, "r", encoding="utf-8") as fp: - for line in fp: - line = line.strip() - if line: - records.append(json.loads(line)) - return records - - -_FALLBACK_INDEX = _load_fallback_index() - - -def _keyword_search(query: str, limit: int = 5) -> list[dict]: - """Rank structures by number of query-token hits across name, signature, - docstring, and file path. Naive but good enough while embeddings build.""" - tokens = [t for t in query.lower().split() if t] - if not tokens or not _FALLBACK_INDEX: - return [] - - scored = [] - for rec in _FALLBACK_INDEX: - haystack = " ".join( - filter( - None, - [ - rec.get("name") or "", - rec.get("signature") or "", - rec.get("docstring") or "", - (rec.get("context") or {}).get("file_path") or "", - (rec.get("context") or {}).get("snippet") or "", - ], - ) - ).lower() - score = sum(haystack.count(t) for t in tokens) - if score: - scored.append((score, rec)) - - scored.sort(key=lambda x: x[0], reverse=True) - - results = [] - for _score, rec in scored[:limit]: - rec = dict(rec) - rec["sub_matches"] = [ - {"overlap_from": rec.get("line_from") or 0, "overlap_to": rec.get("line_to") or 0} - ] - results.append(rec) - return results - - -@app.get("/api/health") -def health(): - return {"status": "ok"} - - -# Both handlers are plain `def` on purpose: the encoders and Qdrant client -# calls are blocking, so FastAPI runs them in its thread pool instead of -# blocking the event loop. -@app.get("/api/search") -def search(query: str): - # Time the work this service is actually responsible for: encoding the query - # and querying Qdrant. Network time is the caller's, and reporting a number - # that moves with the viewer's connection would make it meaningless. The UI - # shows this rather than asserting a figure, so it cannot go stale. - started = time.perf_counter() - try: - results = searcher.search(query, limit=5) - return { - "result": results, - "latency_ms": round((time.perf_counter() - started) * 1000), - "indexed_commit": INDEXED_COMMIT, - } - except Exception as exc: - message = str(exc) - if "doesn't exist" in message or "Not found" in message or "404" in message: - # Collection not built yet. Fall back to keyword ranking so the - # frontend stays usable during the initial indexing run - but only - # when there is an index to rank against. data/ is gitignored, so a - # deployed image has no structures.json and the fallback returns - # nothing. Reporting that as a 200 made a missing collection look - # exactly like a query with no matches, which is how the demo sat - # broken without anyone noticing. - results = _keyword_search(query, limit=5) - if results or _FALLBACK_INDEX: - return { - "result": results, - "mode": "keyword", - "latency_ms": round((time.perf_counter() - started) * 1000), - } - raise HTTPException( - status_code=503, - detail=( - "Search index is unavailable: the Qdrant collection is missing " - "and no local fallback index is present. Run the indexing " - "workflow to populate it." - ), - ) - raise HTTPException(status_code=500, detail=message) - - -@app.get("/api/file") -def file(path: str): - return { - "result": get_file.get(path) - } - - -# Serve the built frontend when it's alongside the backend (self-hosted mode). -# In split deployments (Vercel + Railway) frontend/dist isn't present and we -# skip this mount so the API returns clean 404s for non-/api paths. -_dist_dir = os.path.join(ROOT_DIR, "frontend", "dist") -if os.path.isdir(_dist_dir): - app.mount("/", StaticFiles(directory=_dist_dir, html=True)) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run( - app, - host="0.0.0.0", - port=int(os.environ.get("PORT", "8000")), - ) diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index ccd0493..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -services: - code_search_demo: - build: . - container_name: code_search_demo - environment: - - QDRANT_URL - - QDRANT_API_KEY - ports: - - "8000:8000" diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index e9a09db..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Frontend build-time config. -# Copy to .env.local for local overrides, or set as Vercel env vars. - -# Backend URL. Leave blank / unset in development to use the Vite proxy. -# In production, set to your Railway (or other) backend URL, no trailing slash. -VITE_API_URL=https://your-backend.up.railway.app diff --git a/frontend/index.html b/frontend/index.html index 83ae7d4..7178879 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,11 +6,11 @@ - Semantic Code Search - Qdrant + Hybrid Code Search - Qdrant true, }); diff --git a/frontend/src/api/search.ts b/frontend/src/api/search.ts index eb76937..dce6a8a 100644 --- a/frontend/src/api/search.ts +++ b/frontend/src/api/search.ts @@ -3,14 +3,10 @@ import { SEARCH_URL } from "./constants"; export type SearchResponse = { /** - * Present when the backend fell back to keyword ranking because the - * unixcoder collection is still building. Missing means semantic. - */ - mode?: "keyword" | "semantic"; - /** - * Server-side time to encode the query and search Qdrant, in milliseconds. - * Excludes network, so it reflects this service rather than the viewer's - * connection. Optional, since an older backend will not send it. + * Server-side time to answer, in milliseconds. The embedding happens inside + * the Qdrant cluster now, so this covers the whole round trip from the + * function to Qdrant and back. Excludes the viewer's own network. Optional, + * since an older backend will not send it. */ latency_ms?: number; /** @@ -19,6 +15,26 @@ export type SearchResponse = { * Optional: an older backend will not send it, and links fall back to master. */ indexed_commit?: string; + /** + * Inference tokens the cluster billed for this search. Small - a query is a + * handful of tokens, and the BM25 leg costs none - but it is the number that + * makes the running cost of the demo checkable rather than asserted. + */ + inference_tokens?: number; + /** + * True when this instance had already answered the same query and replayed + * the stored response. The index is a snapshot, so a replay is the same + * answer, not a stale one. + */ + cached?: boolean; + /** + * Which models answered. Sent by the backend rather than hardcoded here, so + * the badge cannot claim one encoder while the index was built with another. + */ + models?: { + dense: string; + sparse: string; + }; result: { code_type: string; context: { @@ -36,10 +52,10 @@ export type SearchResponse = { name: string; signature: string; /** - * Line ranges where the two models agreed, used to highlight inside the + * Line ranges where the two searches agreed, used to highlight inside the * snippet. Only present when a result's file also came back from the code - * search, which is a minority of them - so this is genuinely optional and - * was previously typed as though it always arrived. + * snippet search, which is a minority of them - so this is genuinely + * optional and was previously typed as though it always arrived. */ sub_matches?: { overlap_from: number; diff --git a/frontend/src/components/CustomHeader/index.tsx b/frontend/src/components/CustomHeader/index.tsx index 60e7a30..c3c47ce 100644 --- a/frontend/src/components/CustomHeader/index.tsx +++ b/frontend/src/components/CustomHeader/index.tsx @@ -53,7 +53,7 @@ export function CustomHeader() { Work? - This demo runs semantic search over the Qdrant codebase. + This demo runs hybrid search over the Qdrant codebase. When you search a codebase, you usually want one of two things: @@ -65,7 +65,7 @@ export function CustomHeader() { Diagram of the two-model search workflow - MiniLM reads the description, UniXcoder reads the code itself. + mxbai reads the description, BM25 reads the identifiers. Combining them finds the relevant method and, where both agree, the exact lines inside it. diff --git a/frontend/src/components/MainSection/Main.module.css b/frontend/src/components/MainSection/Main.module.css index 244f852..53cb39e 100644 --- a/frontend/src/components/MainSection/Main.module.css +++ b/frontend/src/components/MainSection/Main.module.css @@ -455,17 +455,12 @@ cursor: help; } -.modePill[data-mode="semantic"] { +.modePill[data-mode="hybrid"] { border: 1px solid rgba(96, 71, 255, 0.4); background: rgba(96, 71, 255, 0.14); color: #b8adff; } -.modePill[data-mode="keyword"] { - border: 1px solid rgba(255, 152, 0, 0.4); - background: rgba(255, 152, 0, 0.14); - color: #ffc37a; -} .errorAlert { max-width: 560px; diff --git a/frontend/src/components/MainSection/index.tsx b/frontend/src/components/MainSection/index.tsx index 10b7d4c..146bf4a 100644 --- a/frontend/src/components/MainSection/index.tsx +++ b/frontend/src/components/MainSection/index.tsx @@ -29,8 +29,8 @@ import classes from "./Main.module.css"; const FEATURES = [ { icon: IconVectorTriangle, - title: "Two Embedding Models", - text: "MiniLM reads natural language, UniXcoder reads code structure. Combined for better matches.", + title: "Dense and Sparse, Fused", + text: "mxbai reads natural language, BM25 matches exact identifiers. Combined for better matches.", }, { icon: IconMessageSearch, @@ -175,15 +175,16 @@ export default function Main() { - {data.mode === "keyword" ? "Warming Up" : "Semantic"} + Hybrid @@ -204,11 +205,11 @@ export default function Main() { )} {showHero && ( - Semantic Search Demo + Hybrid Search Demo Search Code by <span className={classes.headingHighlight}>Meaning</span>, <br /> - Not Keywords + and by Name Describe what code does. Find matching functions and snippets diff --git a/frontend/vercel.json b/frontend/vercel.json deleted file mode 100644 index 457b4d0..0000000 --- a/frontend/vercel.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "buildCommand": "npm run build", - "outputDirectory": "dist", - "framework": "vite", - "rewrites": [ - { "source": "/((?!api/)(?!.*\\.).*)", "destination": "/index.html" } - ] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index cff33f2..2318d63 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -13,8 +13,11 @@ export default defineConfig({ server: { port: Number(process.env.PORT) || 5173, proxy: { + // `vercel dev` runs the /api functions locally on 3000. Start it in + // another terminal, or just run `vercel dev` on its own - it serves the + // Vite app too. "/api": { - target: "http://0.0.0.0:8000", + target: "http://127.0.0.1:3000", changeOrigin: true, }, }, diff --git a/images/architecture-diagram.png b/images/architecture-diagram.png deleted file mode 100644 index 82dc15ef0f8f5401b55c1db845ffc633fc5748bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 273123 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

6mOg81-PMA6+ez%Qn=i# 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

g+j9sLY{a*L)&-i&X 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%0DtZQB%0%Le+Xw&N&t3 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 diff --git a/images/architecture.svg b/images/architecture.svg new file mode 100644 index 0000000..981835d --- /dev/null +++ b/images/architecture.svg @@ -0,0 +1,59 @@ + + Code search architecture: one Vercel deployment, one Qdrant Cloud cluster + + + + + + + + + + + + Browser + one origin + + + + VERCEL + one project, one deployment + + + React app (static) + frontend/ — built by Vite + + + API functions + api/search.ts · api/file.ts + api/health.ts + no model runtime, no dependencies + + + + QDRANT CLOUD + one cluster + + + Cloud Inference + mxbai-embed-large-v1 (dense, 1024d) + BM25 (sparse, computed in-engine) + query text in, vectors never leave + + + Collections + code-signatures-cloud — 17k + code-snippets-cloud — 123k + code-files-cloud — 1.7k, no vectors + dense + sparse per point, + fused server-side with RRF + + + + + query text + + ranked hits + + Two vendors. The previous build had three: the middle box was a container on Railway holding torch, transformers and UniXcoder. + diff --git a/indexer/build.py b/indexer/build.py new file mode 100644 index 0000000..735faa7 --- /dev/null +++ b/indexer/build.py @@ -0,0 +1,361 @@ +"""Build the three collections, with the cluster doing every embedding. + + python indexer/build.py --source qdrant # copy from the old demo's collections + python indexer/build.py --source files # from data/*.json (tools/index_qdrant.sh) + python indexer/build.py --only signatures # one collection at a time + python indexer/build.py --source qdrant --dry-run --limit 200 + +Nothing here loads a model. Each point carries its text and the name of a model +that lives inside the Qdrant cluster, and the cluster returns the vectors. That +is the entire difference between this and the old indexer, and it is what lets +the repository drop torch, transformers, sentence-transformers, the Dockerfile +and the machine that used to run them. + +Runs are resumable. A batch that has been acknowledged is recorded, so a run +interrupted at hour four picks up where it stopped instead of paying to embed +everything again. +""" + +import argparse +import json +import os +import sys +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from qdrant_rest import call, scroll, tokens_by_model # noqa: E402 +from textifier import textify # noqa: E402 + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +DATA_DIR = os.environ.get("DATA_DIR", os.path.join(ROOT, "data")) +STATE_DIR = os.path.join(HERE, ".state") + +DENSE_MODEL = os.environ.get("QDRANT_DENSE_MODEL", "mixedbread-ai/mxbai-embed-large-v1") +SPARSE_MODEL = os.environ.get("QDRANT_SPARSE_MODEL", "Qdrant/bm25") + +# Cut every document to this many characters before embedding. 0 disables it. +# +# This exists because Qdrant Cloud Inference truncates at 512 tokens for every model, +# and all-MiniLM-L6-v2 has a published max_seq_length of 256 - it was trained at +# that length, and positions past it are ones it barely saw. Feeding it the full +# 512 costs real recall on code: bench/truncation.py measures 0.863 docstring +# recall@10 on full text against 0.937 when the input is cut to fit. 700 +# characters is roughly 256 tokens of Rust. +# +# Models whose own limit is 512, mxbai-embed-large-v1 among them, do not need +# this and should run with it set to 0. +TEXT_CHAR_BUDGET = int(os.environ.get("INDEX_CHAR_BUDGET", "0")) + +# Dimensions are a property of the model, so they are looked up rather than +# configured. A mismatch here is only discovered as "expected dim X, got Y" on +# the first upsert, tens of thousands of points into a run. +DENSE_DIMS = { + "sentence-transformers/all-MiniLM-L6-v2": 384, + "mixedbread-ai/mxbai-embed-large-v1": 1024, + "qdrant/clip-vit-b-32-text": 512, +} + +CODE_COLLECTION = os.environ.get("QDRANT_CODE_COLLECTION", "code-snippets-cloud") +NLU_COLLECTION = os.environ.get("QDRANT_NLU_COLLECTION", "code-signatures-cloud") +FILE_COLLECTION = os.environ.get("QDRANT_FILE_COLLECTION", "code-files-cloud") + +# Old collections, read from when --source qdrant. Never written to. +OLD_CODE = os.environ.get("OLD_CODE_COLLECTION", "code-snippets-unixcoder") +OLD_NLU = os.environ.get("OLD_NLU_COLLECTION", "code-signatures") +OLD_FILES = os.environ.get("OLD_FILE_COLLECTION", "code-files") + +BATCH = int(os.environ.get("INDEX_BATCH", "16")) +WORKERS = int(os.environ.get("INDEX_WORKERS", "6")) + + +# --------------------------------------------------------------------------- # +# sources + + +def snippets_from_qdrant(): + for payload in scroll(OLD_CODE): + text = payload.get("code_snippet") + if text: + yield text, payload + + +def snippets_from_files(): + path = os.path.join(DATA_DIR, "qdrant_snippets.jsonl") + # The old indexer took the first of these that was present, so the same + # order is kept here - changing it would silently re-embed the corpus + # against different text and make the two demos incomparable. + keys = ("code_snippet", "body", "signature", "name") + with open(path, encoding="utf-8") as fp: + for line in fp: + if not line.strip(): + continue + row = json.loads(line) + body = next((row[k] for k in keys if row.get(k)), None) + if not body: + continue + docstring = row.get("docstring") or "" + yield f"{docstring} {body}".strip(), row + + +def signatures_from_qdrant(): + for payload in scroll(OLD_NLU): + yield textify(payload), payload + + +def signatures_from_files(): + path = os.path.join(DATA_DIR, "structures.json") + with open(path, encoding="utf-8") as fp: + for line in fp: + if line.strip(): + row = json.loads(line) + yield textify(row), row + + +def files_from_qdrant(): + for payload in scroll(OLD_FILES): + yield None, payload + + +def files_from_files(): + path = os.path.join(DATA_DIR, "rs_files.json") + with open(path, encoding="utf-8") as fp: + for row in json.load(fp): + yield None, row + + +TARGETS = { + "snippets": { + "collection": CODE_COLLECTION, + "vectors": True, + "qdrant": snippets_from_qdrant, + "files": snippets_from_files, + "payload_index": [], + }, + "signatures": { + "collection": NLU_COLLECTION, + "vectors": True, + "qdrant": signatures_from_qdrant, + "files": signatures_from_files, + "payload_index": [], + }, + "files": { + "collection": FILE_COLLECTION, + "vectors": False, + "qdrant": files_from_qdrant, + "files": files_from_files, + # /api/file is a filtered scroll on `path` and nothing else reads this + # collection. Clusters with strict mode on refuse to filter an unindexed + # field, so without this the file viewer fails with a 500 on every + # result anyone clicks, while search itself keeps working - which makes + # it look like a frontend problem. + "payload_index": [("path", "keyword")], + }, +} + + +# --------------------------------------------------------------------------- # +# collection setup + + +def create(collection, with_vectors): + if with_vectors: + size = DENSE_DIMS.get(DENSE_MODEL) + if size is None: + raise SystemExit( + f"Unknown dimensions for {DENSE_MODEL}. Add it to DENSE_DIMS; the " + "Inference tab of the cluster page in the Cloud Console lists them." + ) + body = { + "vectors": {"dense": {"size": size, "distance": "Cosine", "on_disk": True}}, + # BM25 needs IDF applied at query time, computed by the engine from + # collection statistics. That is also why the sparse leg costs no + # inference tokens. + "sparse_vectors": {"sparse": {"modifier": "idf"}}, + "quantization_config": { + "scalar": {"type": "int8", "always_ram": True, "quantile": 0.99} + }, + } + else: + # No vectors at all. This collection is the demo's file store, which is + # here so that Qdrant is the only thing holding state. + body = {"vectors": {}} + + call("DELETE", f"/collections/{collection}") + call("PUT", f"/collections/{collection}", body) + + +def make_point(idx, text, payload, with_vectors): + if not with_vectors: + return {"id": idx, "vector": {}, "payload": payload} + # Both legs read the same text. If the dense model is not being shown the + # tail of a document, the lexical leg should not be either, or the two are + # ranking different corpora. + if TEXT_CHAR_BUDGET: + text = text[:TEXT_CHAR_BUDGET] + return { + "id": idx, + "vector": { + "dense": {"text": text, "model": DENSE_MODEL}, + "sparse": {"text": text, "model": SPARSE_MODEL}, + }, + "payload": payload, + } + + +# --------------------------------------------------------------------------- # +# run + + +def state_path(collection): + return os.path.join(STATE_DIR, f"{collection}.json") + + +def load_state(collection, fresh): + if fresh: + return set() + try: + with open(state_path(collection), encoding="utf-8") as fp: + return set(json.load(fp)["done"]) + except (FileNotFoundError, KeyError, json.JSONDecodeError): + return set() + + +def save_state(collection, done): + os.makedirs(STATE_DIR, exist_ok=True) + with open(state_path(collection), "w", encoding="utf-8") as fp: + json.dump({"done": sorted(done)}, fp) + + +def batches(rows, size): + batch = [] + for item in rows: + batch.append(item) + if len(batch) == size: + yield batch + batch = [] + if batch: + yield batch + + +def run(name, source, args): + target = TARGETS[name] + collection = target["collection"] + with_vectors = target["vectors"] + + print(f"\n=== {name} -> {collection} (source: {source})") + rows = target[source]() + if args.limit: + rows = (r for i, r in enumerate(rows) if i < args.limit) + + done = load_state(collection, fresh=args.fresh) + if args.fresh or not done: + if args.dry_run: + print(f" would recreate {collection}") + else: + create(collection, with_vectors) + print(f" created {collection}") + elif done: + print(f" resuming, {len(done)} batches already uploaded") + + lock = threading.Lock() + totals, counts = {}, {"points": 0, "batches": 0, "skipped": 0} + started = time.perf_counter() + + def upload(offset, batch): + points = [ + make_point(offset + i, text, payload, with_vectors) + for i, (text, payload) in enumerate(batch) + ] + _res, usage = call("PUT", f"/collections/{collection}/points?wait=true", {"points": points}) + with lock: + done.add(offset) + counts["points"] += len(points) + counts["batches"] += 1 + for model, n in tokens_by_model(usage).items(): + totals[model] = totals.get(model, 0) + n + if counts["batches"] % 50 == 0: + elapsed = time.perf_counter() - started + rate = counts["points"] / elapsed if elapsed else 0 + print(f" {counts['points']:>7} points {rate:6.1f}/s", flush=True) + save_state(collection, done) + + # Resume works by batch offset, which is only meaningful because both + # sources yield in a stable order: scroll returns points by id, and the + # files are read start to end. Change either and a resumed run would write + # different documents to ids it thinks are already done. + try: + with ThreadPoolExecutor(max_workers=WORKERS) as pool: + futures = [] + for offset, batch in ((i * BATCH, b) for i, b in enumerate(batches(rows, BATCH))): + if offset in done: + counts["skipped"] += 1 + continue + if args.dry_run: + counts["points"] += len(batch) + counts["batches"] += 1 + continue + futures.append(pool.submit(upload, offset, batch)) + # Bound the queue so a 123k-point run does not materialise every + # batch in memory before the first one finishes. + if len(futures) >= WORKERS * 4: + for future in as_completed(futures): + future.result() + futures = [] + for future in as_completed(futures): + future.result() + finally: + # Save on the way out however the run ended. Losing the last few + # hundred acknowledged batches to an exception means paying to embed + # them again, which is the one cost this script can actually waste. + if not args.dry_run: + save_state(collection, done) + + if not args.dry_run: + for field, schema in target["payload_index"]: + call( + "PUT", + f"/collections/{collection}/index?wait=true", + {"field_name": field, "field_schema": schema}, + ) + print(f" indexed payload field `{field}` as {schema}") + + elapsed = time.perf_counter() - started + print( + f" {counts['points']} points in {elapsed:.0f}s" + + (f", {counts['skipped']} batches skipped" if counts["skipped"] else "") + ) + if totals: + print(f" inference tokens: {json.dumps(totals)}") + return {"collection": collection, "points": counts["points"], "seconds": round(elapsed, 1), + "tokens": totals} + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--source", choices=("qdrant", "files"), default="qdrant", + help="read the corpus from the old collections, or from data/*.json") + ap.add_argument("--only", choices=tuple(TARGETS), action="append", + help="build one collection; repeatable") + ap.add_argument("--limit", type=int, help="stop after N records, for a smoke run") + ap.add_argument("--fresh", action="store_true", + help="recreate the collection and ignore any saved progress") + ap.add_argument("--dry-run", action="store_true", + help="count the work without writing anything") + args = ap.parse_args() + + names = args.only or list(TARGETS) + print(f"dense: {DENSE_MODEL} sparse: {SPARSE_MODEL} batch: {BATCH} x {WORKERS} workers") + print(f"text cut to {TEXT_CHAR_BUDGET} chars" if TEXT_CHAR_BUDGET else "text not cut") + + summary = [run(name, args.source, args) for name in names] + + print("\n" + json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/indexer/paths.py b/indexer/paths.py new file mode 100644 index 0000000..fd5f2d2 --- /dev/null +++ b/indexer/paths.py @@ -0,0 +1,11 @@ +"""Where the corpus files live. + +Split out so the prepare scripts do not have to import anything that touches +Qdrant. They run before there is a cluster to talk to. +""" + +import os + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) +DATA_DIR = os.environ.get("DATA_DIR", os.path.join(ROOT, "data")) diff --git a/code_search/index/convert_lsif_index.py b/indexer/prepare/convert_lsif_index.py similarity index 95% rename from code_search/index/convert_lsif_index.py rename to indexer/prepare/convert_lsif_index.py index dc94069..98d08de 100644 --- a/code_search/index/convert_lsif_index.py +++ b/indexer/prepare/convert_lsif_index.py @@ -4,7 +4,12 @@ from urllib.parse import unquote, urlparse from urllib.request import url2pathname -from code_search.config import DATA_DIR +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from paths import DATA_DIR LSIF_INDEX = Path(DATA_DIR) / "index.lsif" diff --git a/code_search/index/files_to_json.py b/indexer/prepare/files_to_json.py similarity index 90% rename from code_search/index/files_to_json.py rename to indexer/prepare/files_to_json.py index 57af45f..f886735 100644 --- a/code_search/index/files_to_json.py +++ b/indexer/prepare/files_to_json.py @@ -2,7 +2,12 @@ import json from pathlib import Path -from code_search.config import DATA_DIR +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from paths import DATA_DIR def process_file(root_dir, file_path): diff --git a/indexer/qdrant_rest.py b/indexer/qdrant_rest.py new file mode 100644 index 0000000..348e04f --- /dev/null +++ b/indexer/qdrant_rest.py @@ -0,0 +1,121 @@ +"""Stdlib-only Qdrant REST access for the indexer. + +There is no qdrant-client dependency here for the same reason there is no torch: +the indexer's whole job is now to post text and let the cluster embed it, and a +client library would be the largest thing in a repository that otherwise has no +runtime at all. It also keeps the indexer runnable on any Python without a wheel +hunt, which matters because the machine this was written on only has 3.14. + +Connections are pooled per thread and reused. A full run is several thousand +requests; opening a TCP and TLS connection for each one produced sporadic +`[WinError 10054] connection forcibly closed` failures that looked like the +cluster misbehaving and were the client's own connection churn. +""" + +import http.client +import json +import os +import random +import threading +import time +import urllib.parse + +QDRANT_URL = os.environ.get("QDRANT_URL", "http://localhost:6333").rstrip("/") +QDRANT_API_KEY = os.environ.get("QDRANT_API_KEY", "") + +if QDRANT_URL.startswith("https://") and not QDRANT_API_KEY: + raise SystemExit( + f"QDRANT_URL is remote ({QDRANT_URL}) but QDRANT_API_KEY is not set. " + "Copy .env.example to .env and fill both in." + ) + +_PARSED = urllib.parse.urlparse(QDRANT_URL) +_HTTPS = _PARSED.scheme == "https" +_HOST = _PARSED.hostname +_PORT = _PARSED.port or (443 if _HTTPS else 6333) + +# Retried statuses. 429 is the inference rate limit and 5xx is the cluster +# briefly refusing work; both are normal over a run of this length and neither +# should abort hours of indexing. +RETRY_STATUS = {429, 500, 502, 503, 504} +MAX_ATTEMPTS = 8 + +_local = threading.local() + + +class QdrantError(RuntimeError): + def __init__(self, message, status=None): + super().__init__(message) + self.status = status + + +def _connection(fresh=False): + """One keep-alive connection per thread, reopened on demand.""" + conn = getattr(_local, "conn", None) + if conn is not None and fresh: + conn.close() + conn = None + if conn is None: + cls = http.client.HTTPSConnection if _HTTPS else http.client.HTTPConnection + conn = cls(_HOST, _PORT, timeout=300) + _local.conn = conn + return conn + + +def call(method, path, body=None, timeout=300): + """One REST call with backoff. Returns (result, usage).""" + data = json.dumps(body).encode() if body is not None else None + headers = {"api-key": QDRANT_API_KEY, "Content-Type": "application/json"} + last = None + + for attempt in range(MAX_ATTEMPTS): + # A connection that failed is not reused. Retrying down a half-closed + # socket fails identically every time, which is how one blip turns into + # a run that never recovers. + conn = _connection(fresh=attempt > 0) + try: + conn.request(method, path, body=data, headers=headers) + response = conn.getresponse() + raw = response.read() + + if response.status in RETRY_STATUS: + last = QdrantError( + f"{method} {path} -> {response.status}: {raw.decode()[:400]}", + response.status, + ) + else: + payload = json.loads(raw) + if "result" not in payload: + raise QdrantError( + f"{method} {path} -> {response.status}: {json.dumps(payload)[:400]}", + response.status, + ) + return payload["result"], payload.get("usage", {}) + except (OSError, http.client.HTTPException, json.JSONDecodeError) as exc: + last = QdrantError(f"{method} {path} -> {type(exc).__name__}: {exc}") + + # Exponential backoff with jitter, floored: pure full jitter can pick a + # delay of almost zero, which retries straight into the same problem. + time.sleep(min(30.0, 2**attempt) * (0.5 + 0.5 * random.random())) + + raise last + + +def tokens_by_model(usage): + models = (usage or {}).get("inference", {}).get("models", {}) + return {name: m.get("tokens", 0) for name, m in models.items()} + + +def scroll(collection, batch=512, with_vector=False): + """Yield every payload in a collection. Read-only.""" + offset = None + while True: + body = {"limit": batch, "with_payload": True, "with_vector": with_vector} + if offset is not None: + body["offset"] = offset + result, _usage = call("POST", f"/collections/{collection}/points/scroll", body) + for point in result["points"]: + yield point["payload"] + offset = result.get("next_page_offset") + if offset is None: + return diff --git a/code_search/index/textifier.py b/indexer/textifier.py similarity index 100% rename from code_search/index/textifier.py rename to indexer/textifier.py diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..219189a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1823 @@ +{ + "name": "demo-code-search-cloud", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo-code-search-cloud", + "devDependencies": { + "@vercel/node": "^5.3.24", + "typescript": "^5.6.3" + } + }, + "node_modules/@edge-runtime/format": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@edge-runtime/format/-/format-2.2.1.tgz", + "integrity": "sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/node-utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/node-utils/-/node-utils-2.3.0.tgz", + "integrity": "sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/ponyfill": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@edge-runtime/ponyfill/-/ponyfill-2.4.2.tgz", + "integrity": "sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/primitives": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/primitives/-/primitives-4.1.0.tgz", + "integrity": "sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@edge-runtime/vm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@edge-runtime/vm/-/vm-3.2.0.tgz", + "integrity": "sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/primitives": "4.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", + "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", + "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", + "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", + "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", + "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", + "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", + "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", + "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", + "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", + "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", + "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", + "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", + "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", + "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", + "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", + "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", + "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", + "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", + "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", + "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", + "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", + "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", + "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", + "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", + "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", + "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", + "integrity": "sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "consola": "^3.2.3", + "detect-libc": "^2.0.0", + "https-proxy-agent": "^7.0.5", + "node-fetch": "^2.6.7", + "nopt": "^8.0.0", + "semver": "^7.5.3", + "tar": "^7.4.0" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + }, + "engines": { + "node": ">=18" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@ts-morph/common": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.11.1.tgz", + "integrity": "sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "minimatch": "^3.0.4", + "mkdirp": "^1.0.4", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/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/node": { + "version": "20.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.0.tgz", + "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@vercel/build-utils": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-14.2.0.tgz", + "integrity": "sha512-GwmtB31tBXQEzFw11grr8BKFCBdUORmYeooB0ZtonaCXZMZaPCHLBFTMFKsvaV6ZciQORPInRwXShbFvmnjqtg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "1.2.3", + "es-module-lexer": "1.5.0" + } + }, + "node_modules/@vercel/build-utils/node_modules/es-module-lexer": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.0.tgz", + "integrity": "sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vercel/error-utils": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@vercel/error-utils/-/error-utils-2.2.1.tgz", + "integrity": "sha512-9DhP8jP7raLML4hGsBemxX5fXuQnu5xxMV+HjGygGbzEmVK/+KyJ3QP2Cw7PdF0uXdb9N0Qa4c3tRGH34ZX6vw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@vercel/nft": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.10.0.tgz", + "integrity": "sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^2.0.0", + "@rollup/pluginutils": "^5.1.3", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.5", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^13.0.0", + "graceful-fs": "^4.2.9", + "node-gyp-build": "^4.2.2", + "picomatch": "^4.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@vercel/node": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.10.2.tgz", + "integrity": "sha512-YBXcoQVOh5O2ySXvzE+POhPEQEPMJJo4ctlMMdp5why/NIoa8m6gotv14j8Uo6D5qyZsnc+0+++JgUiV4mYB6w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@edge-runtime/node-utils": "2.3.0", + "@edge-runtime/primitives": "4.1.0", + "@edge-runtime/vm": "3.2.0", + "@types/node": "20.11.0", + "@vercel/build-utils": "14.2.0", + "@vercel/error-utils": "2.2.1", + "@vercel/nft": "1.10.0", + "@vercel/static-config": "3.4.1", + "async-listen": "3.0.0", + "cjs-module-lexer": "1.2.3", + "edge-runtime": "2.5.9", + "es-module-lexer": "1.4.1", + "esbuild": "0.27.0", + "etag": "1.8.1", + "mime-types": "2.1.35", + "node-fetch": "2.6.9", + "path-to-regexp": "6.1.0", + "path-to-regexp-updated": "npm:path-to-regexp@6.3.0", + "ts-morph": "12.0.0", + "tsx": "4.21.0", + "typescript": "npm:typescript@5.9.3", + "undici": "5.28.4" + } + }, + "node_modules/@vercel/static-config": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.4.1.tgz", + "integrity": "sha512-kJKTyOg25JDRgDkHEkc+vWlvURxmSQkVKyRPO4EEGD/8HpJT+4u9Z/VGxwnCZ6zZBxYPpma283qBsHwY0gXjfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ajv": "8.6.3", + "json-schema-to-ts": "1.6.4", + "ts-morph": "12.0.0" + } + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/async-listen": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.0.tgz", + "integrity": "sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/code-block-writer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-10.1.1.tgz", + "integrity": "sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/convert-hrtime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-3.0.0.tgz", + "integrity": "sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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/edge-runtime": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", + "integrity": "sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "@edge-runtime/format": "2.2.1", + "@edge-runtime/ponyfill": "2.4.2", + "@edge-runtime/vm": "3.2.0", + "async-listen": "3.0.1", + "mri": "1.2.0", + "picocolors": "1.0.0", + "pretty-ms": "7.0.1", + "signal-exit": "4.0.2", + "time-span": "4.0.0" + }, + "bin": { + "edge-runtime": "dist/cli/index.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/edge-runtime/node_modules/async-listen": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/async-listen/-/async-listen-3.0.1.tgz", + "integrity": "sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/es-module-lexer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", + "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.0", + "@esbuild/android-arm": "0.27.0", + "@esbuild/android-arm64": "0.27.0", + "@esbuild/android-x64": "0.27.0", + "@esbuild/darwin-arm64": "0.27.0", + "@esbuild/darwin-x64": "0.27.0", + "@esbuild/freebsd-arm64": "0.27.0", + "@esbuild/freebsd-x64": "0.27.0", + "@esbuild/linux-arm": "0.27.0", + "@esbuild/linux-arm64": "0.27.0", + "@esbuild/linux-ia32": "0.27.0", + "@esbuild/linux-loong64": "0.27.0", + "@esbuild/linux-mips64el": "0.27.0", + "@esbuild/linux-ppc64": "0.27.0", + "@esbuild/linux-riscv64": "0.27.0", + "@esbuild/linux-s390x": "0.27.0", + "@esbuild/linux-x64": "0.27.0", + "@esbuild/netbsd-arm64": "0.27.0", + "@esbuild/netbsd-x64": "0.27.0", + "@esbuild/openbsd-arm64": "0.27.0", + "@esbuild/openbsd-x64": "0.27.0", + "@esbuild/openharmony-arm64": "0.27.0", + "@esbuild/sunos-x64": "0.27.0", + "@esbuild/win32-arm64": "0.27.0", + "@esbuild/win32-ia32": "0.27.0", + "@esbuild/win32-x64": "0.27.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-1.6.4.tgz", + "integrity": "sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.6", + "ts-toolbelt": "^6.15.5" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parse-ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-2.1.0.tgz", + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "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-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp-updated": { + "name": "path-to-regexp", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true, + "license": "ISC" + }, + "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/pretty-ms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-7.0.1.tgz", + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar": { + "version": "7.5.22", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", + "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/time-span": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-4.0.0.tgz", + "integrity": "sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ts-morph": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-12.0.0.tgz", + "integrity": "sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.11.0", + "code-block-writer": "^10.1.1" + } + }, + "node_modules/ts-toolbelt": { + "version": "6.15.5", + "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-6.15.5.tgz", + "integrity": "sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "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/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..0949906 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "demo-code-search-cloud", + "private": true, + "type": "module", + "scripts": { + "build": "npm --prefix frontend ci && npm --prefix frontend run build", + "typecheck": "tsc --noEmit", + "test": "esbuild api/_lib/merge.ts --format=esm --outfile=.test-build/merge.mjs && node --test test/*.test.mjs" + }, + "devDependencies": { + "@vercel/node": "^5.3.24", + "typescript": "^5.6.3" + } +} diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index c8e30b3..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[tool.poetry] -name = "demo-code-search" -version = "0.1.0" -description = "Semantic code search demo over the Qdrant codebase" -authors = ["Qdrant Team "] - -[tool.poetry.dependencies] -python = ">=3.9,<3.13" -fastapi = "^0.115.14" -uvicorn = "^0.34.3" -torch = "^2.6.0" -transformers = "^4.49.0" -qdrant-client = "^1.12.0" -python-dotenv = "^1.0.1" -sentence-transformers = "^2.7.0" -tqdm = "^4.67.1" -numpy = "^1.26.4,<2" - -[tool.poetry.dev-dependencies] - -[build-system] -requires = ["poetry-core>=1.0.0"] -build-backend = "poetry.core.masonry.api" diff --git a/railway.json b/railway.json deleted file mode 100644 index 4dec213..0000000 --- a/railway.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://railway.app/railway.schema.json", - "build": { - "builder": "DOCKERFILE", - "dockerfilePath": "Dockerfile" - }, - "deploy": { - "healthcheckPath": "/api/health", - "healthcheckTimeout": 300, - "restartPolicyType": "ON_FAILURE", - "restartPolicyMaxRetries": 3 - } -} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dcfea77..0000000 --- a/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ ---extra-index-url https://download.pytorch.org/whl/cpu -torch==2.6.0+cpu -fastapi==0.115.14 ; python_version >= "3.9" and python_version < "3.13" -uvicorn==0.34.3 ; python_version >= "3.9" and python_version < "3.13" -transformers==4.49.0 ; python_version >= "3.9" and python_version < "3.13" -qdrant-client==1.12.0 ; python_version >= "3.9" and python_version < "3.13" -python-dotenv==1.0.1 ; python_version >= "3.9" and python_version < "3.13" -sentence-transformers==2.7.0 ; python_version >= "3.9" and python_version < "3.13" -tqdm==4.67.1 ; python_version >= "3.9" and python_version < "3.13" -numpy==1.26.4 ; python_version >= "3.9" and python_version < "3.13" diff --git a/test/merge.test.mjs b/test/merge.test.mjs new file mode 100644 index 0000000..ed5e52e --- /dev/null +++ b/test/merge.test.mjs @@ -0,0 +1,106 @@ +/** + * The merge step is the only non-trivial logic in the API, and it was ported + * from Python. These cases were checked against the original implementation on + * a randomised fixture and matched exactly; they are pinned here so a later + * refactor cannot quietly change the ordering or shift a highlight by a line. + * + * Run with: npm test + */ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { mergeSearchResults, overlappingSnippets } from "../.test-build/merge.mjs"; + +const nluHit = (name, filePath, from, to) => ({ + code_type: "Function", + context: { + file_name: "x.rs", + file_path: filePath, + module: "m", + snippet: "s", + struct_name: null, + }, + docstring: null, + line: from, + line_from: from, + line_to: to, + name, + signature: "sig", +}); + +test("snippet lines are 0-based and signature lines are 1-based", () => { + // A snippet covering rows 9..29 in the snippet collection is lines 10..30 in + // the signature collection. Dropping the +1 shifts every highlight up by one, + // which renders as a nearly-right answer and is easy to miss in review. + const overlaps = overlappingSnippets( + [{ file: "a.rs", start_line: 9, end_line: 29 }], + nluHit("f", "a.rs", 30, 40), + ); + assert.deepEqual(overlaps, [{ overlap_from: 30, overlap_to: 30 }]); +}); + +test("a snippet that ends before the signature starts does not overlap", () => { + const overlaps = overlappingSnippets( + [{ file: "a.rs", start_line: 1, end_line: 5 }], + nluHit("f", "a.rs", 30, 40), + ); + assert.deepEqual(overlaps, []); +}); + +test("overlaps are reported in source order regardless of input order", () => { + const overlaps = overlappingSnippets( + [ + { file: "a.rs", start_line: 34, end_line: 36 }, + { file: "a.rs", start_line: 30, end_line: 31 }, + ], + nluHit("f", "a.rs", 30, 40), + ); + assert.deepEqual(overlaps, [ + { overlap_from: 31, overlap_to: 32 }, + { overlap_from: 35, overlap_to: 37 }, + ]); +}); + +test("the search ranking survives the merge", () => { + // The original sorted by highlight count here, which promoted "b" to the top + // purely for having two overlapping snippet hits. Over 300 docstring queries + // that cost 0.273 recall@1, because the result the search ranked first is + // usually the right one and highlight count does not know that. + const code = [ + { file: "b.rs", start_line: 0, end_line: 100 }, + { file: "b.rs", start_line: 5, end_line: 20 }, + { file: "c.rs", start_line: 0, end_line: 100 }, + ]; + const nlu = [ + nluHit("a", "a.rs", 1, 50), // ranked first, no snippet hits in its file + nluHit("c", "c.rs", 1, 50), // one + nluHit("b", "b.rs", 1, 50), // two + ]; + assert.deepEqual( + mergeSearchResults(code, nlu).map((h) => h.name), + ["a", "c", "b"], + ); +}); + +test("highlights are still attached to whichever results have them", () => { + const code = [{ file: "c.rs", start_line: 0, end_line: 100 }]; + const nlu = [nluHit("a", "a.rs", 1, 50), nluHit("c", "c.rs", 1, 50)]; + const [first, second] = mergeSearchResults(code, nlu); + assert.equal("sub_matches" in first, false); + assert.deepEqual(second.sub_matches, [{ overlap_from: 1, overlap_to: 50 }]); +}); + +test("a result with no snippet hits keeps no sub_matches key at all", () => { + // The frontend types this as optional and renders unhighlighted when absent. + // An empty array would be a different thing: agreement on zero lines. + const [hit] = mergeSearchResults([], [nluHit("a", "a.rs", 1, 50)]); + assert.equal("sub_matches" in hit, false); +}); + +test("order is untouched when nothing has highlights", () => { + const nlu = [nluHit("first", "a.rs", 1, 5), nluHit("second", "b.rs", 1, 5)]; + assert.deepEqual( + mergeSearchResults([], nlu).map((h) => h.name), + ["first", "second"], + ); +}); diff --git a/tools/download_and_index.sh b/tools/download_and_index.sh index 90f323d..e64fb6a 100644 --- a/tools/download_and_index.sh +++ b/tools/download_and_index.sh @@ -9,17 +9,16 @@ git clone https://github.com/qdrant/qdrant.git /tmp/qdrant INDEXED_COMMIT=$(git -C /tmp/qdrant rev-parse HEAD) -QDRANT_PATH=/tmp/qdrant bash -x tools/index_qdrant.sh /tmp/qdrant +bash -x tools/index_qdrant.sh /tmp/qdrant rm -rf /tmp/qdrant -# Result links carry line numbers computed from this commit, so the backend has -# to resolve them against it rather than against a branch that keeps moving. +# Result links carry line numbers computed from this commit, so the API has to +# resolve them against it rather than against a branch that keeps moving. # Printed last so it is the final thing in the log, where it is easy to find. echo echo "==============================================================" echo "Indexed qdrant/qdrant at commit: $INDEXED_COMMIT" -echo "Set INDEXED_COMMIT to this value on the API service so result" -echo "links point at the code that was actually indexed." +echo "Set INDEXED_COMMIT to this value in the Vercel project so" +echo "result links point at the code that was actually indexed." echo "==============================================================" - diff --git a/tools/index_qdrant.sh b/tools/index_qdrant.sh index 02e3d09..7be7442 100644 --- a/tools/index_qdrant.sh +++ b/tools/index_qdrant.sh @@ -1,26 +1,30 @@ #!/usr/bin/env bash +# +# Build the corpus from a checkout of qdrant/qdrant and load it into Qdrant. +# +# The embedding happens inside the cluster, so nothing here installs a model. +# The slow parts are rust-analyzer's LSIF pass and the rust-parser container, +# which is where most of the wall clock goes. set -e -QDRANT_PATH=$1 +QDRANT_PATH=$(realpath "$1") -QDRANT_PATH=$(realpath $QDRANT_PATH) - -# Get path to this script SCRIPT_PATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" ROOT_PATH=$SCRIPT_PATH/.. +export QDRANT_PATH -python -m code_search.index.files_to_json - -python -m code_search.index.file_uploader - -rustup run stable rust-analyzer -v lsif $QDRANT_PATH > $ROOT_PATH/data/index.lsif - -python -m code_search.index.convert_lsif_index +# Whole .rs files, for the file viewer. +python "$ROOT_PATH/indexer/prepare/files_to_json.py" -python -m code_search.index.upload_code +# Folding ranges -> code snippets. +rustup run stable rust-analyzer -v lsif "$QDRANT_PATH" > "$ROOT_PATH/data/index.lsif" +python "$ROOT_PATH/indexer/prepare/convert_lsif_index.py" -docker run --rm -v $QDRANT_PATH:/source qdrant/rust-parser ./rust_parser /source > $ROOT_PATH/data/structures.json +# Function and struct signatures with their docstrings. +docker run --rm -v "$QDRANT_PATH":/source qdrant/rust-parser ./rust_parser /source \ + > "$ROOT_PATH/data/structures.json" -python -m code_search.index.upload_signatures +# One pass, three collections, every vector computed by Qdrant Cloud Inference. +python "$ROOT_PATH/indexer/build.py" --source files --fresh diff --git a/tools/migrate_to_qdrant_cloud.py b/tools/migrate_to_qdrant_cloud.py deleted file mode 100644 index bef149a..0000000 --- a/tools/migrate_to_qdrant_cloud.py +++ /dev/null @@ -1,77 +0,0 @@ -""" -Copy the three collections used by this app from a source Qdrant instance -(typically your local Docker container) to a target Qdrant Cloud cluster. - -Usage: - export SRC_URL=http://localhost:6333 - export DST_URL=https://your-cluster.aws.cloud.qdrant.io:6333 - export DST_API_KEY=... - python -m tools.migrate_to_qdrant_cloud -""" - -import os -import sys - -from qdrant_client import QdrantClient - -COLLECTIONS = [ - "code-files", - "code-signatures", - "code-snippets-unixcoder", -] - -BATCH = 128 - - -def main() -> int: - src_url = os.environ.get("SRC_URL", "http://localhost:6333") - dst_url = os.environ.get("DST_URL") - dst_api_key = os.environ.get("DST_API_KEY") - if not dst_url or not dst_api_key: - print("DST_URL and DST_API_KEY must be set", file=sys.stderr) - return 1 - - src = QdrantClient(url=src_url) - dst = QdrantClient(url=dst_url, api_key=dst_api_key) - - for name in COLLECTIONS: - if not src.collection_exists(name): - print(f"skip {name}: not in source") - continue - - info = src.get_collection(name) - print(f"migrating {name} ({info.points_count} points)") - - if dst.collection_exists(name): - dst.delete_collection(name) - dst.create_collection( - collection_name=name, - vectors_config=info.config.params.vectors, - ) - - offset = None - migrated = 0 - while True: - points, next_offset = src.scroll( - collection_name=name, - limit=BATCH, - with_payload=True, - with_vectors=True, - offset=offset, - ) - if not points: - break - dst.upsert(collection_name=name, points=points) - migrated += len(points) - print(f" {migrated}/{info.points_count}") - if next_offset is None: - break - offset = next_offset - - print(f"done {name}: {migrated} points") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..57dfed7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["api/**/*.ts"] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..8a3b70a --- /dev/null +++ b/vercel.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "buildCommand": "npm run build", + "outputDirectory": "frontend/dist", + "framework": null, + "functions": { + "api/*.ts": { + "maxDuration": 20 + } + }, + "rewrites": [ + { + "source": "/((?!api/)(?!.*\\.).*)", + "destination": "/index.html" + } + ], + "regions": [ + "pdx1" + ] +} From f43c259d6b56b6aa374ebbbc51e180605555a8d7 Mon Sep 17 00:00:00 2001 From: John Kupchanko Date: Wed, 2 Sep 2026 17:03:53 -0700 Subject: [PATCH 2/2] Clear the deprecated packages and vulnerabilities out of the build Six deprecation warnings in the Vercel build log and eight npm audit vulnerabilities, several high. Inherited, but visible to anyone reading a build. eslint 8 is end of life and pulled in five of the six deprecations, so this moves to eslint 10 and flat config. vite 5 to 8, react-router-dom 6 to 7, postcss-preset-mantine 1.6 to 1.18. @mantine/core stays at 7.1.0 because it renders the approved design. Zero deprecations on a clean install, zero vulnerabilities. The newer react-hooks plugin flagged two real patterns, both fixed rather than silenced: useTypewriter set state synchronously in an effect, and the search field built its Enter handler during render. Build, lint, typecheck and tests pass, the page renders unchanged, and Enter still runs a search. --- frontend/.eslintrc.cjs | 14 - frontend/eslint.config.js | 29 + frontend/package-lock.json | 3243 ++++++++--------- frontend/package.json | 23 +- frontend/src/components/MainSection/index.tsx | 10 +- frontend/src/hooks/useTypewriter.ts | 34 +- package.json.frontend.bak | 38 + 7 files changed, 1670 insertions(+), 1721 deletions(-) delete mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/eslint.config.js create mode 100644 package.json.frontend.bak diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs deleted file mode 100644 index 46570f3..0000000 --- a/frontend/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - parser: "@typescript-eslint/parser", - parserOptions: { ecmaVersion: "latest", sourceType: "module" }, - plugins: ["react-refresh"], - rules: { - "react-refresh/only-export-components": "warn", - }, -}; diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..969e57d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +// Flat config. ESLint 9 dropped .eslintrc, and staying on 8 meant shipping six +// deprecated transitive packages (inflight, glob@7, rimraf@3, and the two +// @humanwhocodes packages) that show up in every Vercel build log. +import js from "@eslint/js"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist", "node_modules"] }, + { + files: ["**/*.{ts,tsx}"], + extends: [js.configs.recommended, ...tseslint.configs.recommended], + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": "warn", + }, + }, +); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b7de08e..5c0d4d3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,505 +15,420 @@ "prism-react-renderer": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^7.18.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^20.8.0", "@types/react": "^18.3.31", "@types/react-dom": "^18.0.11", - "@typescript-eslint/eslint-plugin": "^5.57.1", - "@typescript-eslint/parser": "^5.57.1", - "@vitejs/plugin-react-swc": "^3.7.2", - "eslint": "^8.38.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "postcss": "^8.4.24", - "postcss-preset-mantine": "1.6.0", + "@vitejs/plugin-react-swc": "^4.3.3", + "eslint": "^10.9.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.6", + "globals": "^17.12.0", + "postcss": "^8.5.26", + "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "typescript": "^5.0.2", - "vite": "^5.4.11" + "typescript-eslint": "^8.69.0", + "vite": "^8.2.2" } }, - "node_modules/@babel/runtime": { + "node_modules/@babel/code-frame": { "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", - "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "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", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "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": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], + "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", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], + "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", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], + "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", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, "engines": { - "node": ">=12" + "node": ">=6.9.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "netbsd" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=12" + "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/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=12" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], + "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": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "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", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "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": "MIT", + "license": "Apache-2.0", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "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.11" + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "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.7.5", - "@floating-ui/utils": "^0.2.11" + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" } }, "node_modules/@floating-ui/react": { @@ -532,12 +447,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "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.7.6" + "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -545,25 +460,47 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "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": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=10.10.0" + "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": { @@ -580,13 +517,69 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "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/@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": "BSD-3-Clause" + "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/@mantine/core": { "version": "7.1.0", @@ -616,64 +609,20 @@ "react": "^18.2.0" } }, - "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==", + "node_modules/@oxc-project/types": { + "version": "0.148.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", + "integrity": "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==", "dev": true, - "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==", - "dev": true, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", - "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", - "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.7.tgz", + "integrity": "sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==", "cpu": [ "arm" ], @@ -682,12 +631,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", - "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.7.tgz", + "integrity": "sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==", "cpu": [ "arm64" ], @@ -696,12 +648,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", - "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.7.tgz", + "integrity": "sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==", "cpu": [ "arm64" ], @@ -710,12 +665,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", - "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.7.tgz", + "integrity": "sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==", "cpu": [ "x64" ], @@ -724,26 +682,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", - "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", - "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.7.tgz", + "integrity": "sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==", "cpu": [ "x64" ], @@ -752,26 +699,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", - "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", - "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.7.tgz", + "integrity": "sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==", "cpu": [ "arm" ], @@ -780,180 +716,135 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", - "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", - "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.7.tgz", + "integrity": "sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", - "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", - "cpu": [ - "loong64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", - "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", - "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.7.tgz", + "integrity": "sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", - "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.7.tgz", + "integrity": "sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", - "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", - "cpu": [ - "riscv64" + "libc": [ + "glibc" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", - "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", - "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.7.tgz", + "integrity": "sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", - "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.7.tgz", + "integrity": "sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", - "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.7.tgz", + "integrity": "sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", - "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", - "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.7.tgz", + "integrity": "sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==", "cpu": [ "arm64" ], @@ -962,12 +853,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", - "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.7.tgz", + "integrity": "sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==", "cpu": [ "arm64" ], @@ -976,26 +870,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", - "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", - "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.7.tgz", + "integrity": "sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==", "cpu": [ "x64" ], @@ -1004,32 +887,28 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", - "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.43.tgz", - "integrity": "sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.16.1.tgz", + "integrity": "sha512-nUaeu91O5QZKrQdaDCHd402ogUIoNOOjpkZNq0UomWK0G6gDaGmLhvddF1/3BXf5O8aLyo6ZPY/aMDWvaJQ/hg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.27" + "@swc/types": "^0.1.28" }, "engines": { "node": ">=10" @@ -1039,18 +918,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.43", - "@swc/core-darwin-x64": "1.15.43", - "@swc/core-linux-arm-gnueabihf": "1.15.43", - "@swc/core-linux-arm64-gnu": "1.15.43", - "@swc/core-linux-arm64-musl": "1.15.43", - "@swc/core-linux-ppc64-gnu": "1.15.43", - "@swc/core-linux-s390x-gnu": "1.15.43", - "@swc/core-linux-x64-gnu": "1.15.43", - "@swc/core-linux-x64-musl": "1.15.43", - "@swc/core-win32-arm64-msvc": "1.15.43", - "@swc/core-win32-ia32-msvc": "1.15.43", - "@swc/core-win32-x64-msvc": "1.15.43" + "@swc/core-darwin-arm64": "1.16.1", + "@swc/core-darwin-x64": "1.16.1", + "@swc/core-linux-arm-gnueabihf": "1.16.1", + "@swc/core-linux-arm64-gnu": "1.16.1", + "@swc/core-linux-arm64-musl": "1.16.1", + "@swc/core-linux-ppc64-gnu": "1.16.1", + "@swc/core-linux-s390x-gnu": "1.16.1", + "@swc/core-linux-x64-gnu": "1.16.1", + "@swc/core-linux-x64-musl": "1.16.1", + "@swc/core-win32-arm64-msvc": "1.16.1", + "@swc/core-win32-ia32-msvc": "1.16.1", + "@swc/core-win32-x64-msvc": "1.16.1" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1062,9 +941,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.43.tgz", - "integrity": "sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.16.1.tgz", + "integrity": "sha512-zlJblJ8ncErD43lKdxjbUaUskJQf+LxiPXYcWXD8/8ZMV+7uuAT+CwjciLXpyZBd5Pq/S726bMpeeAwSeL1hhg==", "cpu": [ "arm64" ], @@ -1079,9 +958,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.43.tgz", - "integrity": "sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.16.1.tgz", + "integrity": "sha512-IN0BmPWb0YAh/17mmlWB/HDBtTw2MfuW4hulf/tQAgTQBRH17l+z499bNJLK6LizSjqs0P7V+jU38Zj+vJC1DA==", "cpu": [ "x64" ], @@ -1096,9 +975,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.43.tgz", - "integrity": "sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.16.1.tgz", + "integrity": "sha512-EYgrx2YOCQ2Twz2S793kqNjPkpvYVUPzzR95bIb7by+VQcyaai4lZZ2iz/tZvcFVKSNcN3/JTKwx+aBn2ZL52A==", "cpu": [ "arm" ], @@ -1113,13 +992,16 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.43.tgz", - "integrity": "sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.16.1.tgz", + "integrity": "sha512-moyKm0YZlHdHohzm1YwgAyesqnE853rO0REMfJLFAova51wF9BNi+3ZW2PeS7Vqvn6HeJuepLpAHbBdZctxpHA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1130,13 +1012,16 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.43.tgz", - "integrity": "sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.16.1.tgz", + "integrity": "sha512-kKGBO9wdapiSzuf5ZzZ2fYtlu1BNSYtIIUxvH1ir/gcelTOREEHGDCLTDFx/2Knf878nU11A40z7LxwasEFxqA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1147,13 +1032,16 @@ } }, "node_modules/@swc/core-linux-ppc64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.43.tgz", - "integrity": "sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.16.1.tgz", + "integrity": "sha512-nZ6qahtLxC3PM54cWOQZHxt4lTCF/3J4LIoWWzz6v7A+rLs8Dx54anYQf7mH3eIi8KlNpgKci/ie8ZSqFN8O7A==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1164,13 +1052,16 @@ } }, "node_modules/@swc/core-linux-s390x-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.43.tgz", - "integrity": "sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.16.1.tgz", + "integrity": "sha512-4ji5PNzhYq193Z4/4xUaSoNJza6iCkDJSzhetrbB6KOYxsr+kxtQr8ePWhMJUiMt6JUWtXaZ1PYT8FhtED+nGA==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1181,13 +1072,16 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.43.tgz", - "integrity": "sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.16.1.tgz", + "integrity": "sha512-VJQxqrisHV+B394IgrOu8YsIIXZgffnf5tO+yc9Z/hoUpuZEvuQTjWwlnpZdpyD+0nx6LTD1/3k646JYm43yJA==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1198,13 +1092,16 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.43.tgz", - "integrity": "sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.16.1.tgz", + "integrity": "sha512-r9oV1mwxxsIGcLV1IQ/tw76MW3doatKze1QFWuC+a7QqJUkhY/bKTSVk6NpKKUGm2LDsE33Va8VqSClfA7vSiQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -1215,9 +1112,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.43.tgz", - "integrity": "sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.16.1.tgz", + "integrity": "sha512-6huNRessoBLxWEqBm5zJXyCQ27TO7anvkdiuQ5MDO4CJni0nOXEqKtV9RllQ2TdyENKKsUMXVnIfW2hIXx/R5Q==", "cpu": [ "arm64" ], @@ -1232,9 +1129,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.43.tgz", - "integrity": "sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.16.1.tgz", + "integrity": "sha512-OVKJFUzphrGmsh+BGtcZDesx0YryV7/Yvy5XGgTqnrZfjnyfcr5uaqYQugCckdIlupc5Vs3XtDjRAj12z4ZPlw==", "cpu": [ "ia32" ], @@ -1249,9 +1146,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.43", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.43.tgz", - "integrity": "sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.16.1.tgz", + "integrity": "sha512-Bt+VIhWYCGk4urklnkkteLUOeLv1VxigwTCeB/xC6rBZxY6IIKdDwCJf6on3E3SUGsIqmQS6QqtuJQc1VxF4Aw==", "cpu": [ "x64" ], @@ -1273,9 +1170,9 @@ "license": "Apache-2.0" }, "node_modules/@swc/types": { - "version": "0.1.27", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.27.tgz", - "integrity": "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==", + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.28.tgz", + "integrity": "sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1309,6 +1206,13 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, + "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.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1367,130 +1271,160 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.69.0.tgz", + "integrity": "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/type-utils": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.69.0", + "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.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz", + "integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.69.0.tgz", + "integrity": "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "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.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.69.0.tgz", + "integrity": "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.69.0", + "@typescript-eslint/types": "^8.69.0", + "debug": "^4.4.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "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": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.69.0.tgz", + "integrity": "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "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.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.69.0.tgz", + "integrity": "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ==", + "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": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.69.0.tgz", + "integrity": "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.69.0.tgz", + "integrity": "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1498,103 +1432,109 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.69.0.tgz", + "integrity": "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@typescript-eslint/project-service": "8.69.0", + "@typescript-eslint/tsconfig-utils": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/visitor-keys": "8.69.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.69.0.tgz", + "integrity": "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.69.0", + "@typescript-eslint/types": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.69.0.tgz", + "integrity": "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.69.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "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.3.2", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", - "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", - "dev": true, - "license": "ISC" - }, "node_modules/@vitejs/plugin-react-swc": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.11.0.tgz", - "integrity": "sha512-YTJCGFdNMHCMfjODYtxRNVAYmTWQ1Lb8PulP/2/f/oEEtglw8oKxKIZmmRkyXrVrHfsKOaVkAc3NT9/dMutO5w==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.3.3.tgz", + "integrity": "sha512-bti8ZAcvz4Lh6/e4Uk2k3aa1TiUXbbMsahuqOHvd3MveFTkKDZOA6wQVkpj7J/+tepX/wGfe+lsGh/t24HTXMQ==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-beta.27", - "@swc/core": "^1.12.11" + "@rolldown/pluginutils": "^1.0.1", + "@swc/core": "^1.15.46" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4 || ^5 || ^6 || ^7" + "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", "bin": { @@ -1643,39 +1583,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "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", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, "node_modules/aria-hidden": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", @@ -1688,16 +1595,6 @@ "node": ">=10" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1705,46 +1602,85 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "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==", + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "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": { - "fill-range": "^7.1.1" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=8" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, "node_modules/call-bind-apply-helpers": { @@ -1760,16 +1696,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1780,22 +1706,26 @@ "node": ">= 6" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "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/clsx": { "version": "2.0.0", @@ -1806,26 +1736,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1838,13 +1748,26 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1913,37 +1836,21 @@ "node": ">=0.4.0" } }, - "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/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "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": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, + "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } + "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/dunder-proto": { "version": "1.0.1", @@ -1959,6 +1866,13 @@ "node": ">= 0.4" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.420", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", + "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "dev": true, + "license": "ISC" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2004,43 +1918,14 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "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, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "node": ">=6" } }, "node_modules/escape-string-regexp": { @@ -2057,152 +1942,139 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@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.2", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "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": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", - "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "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": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "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-plugin-react-refresh": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.3.5.tgz", - "integrity": "sha512-61qNIsc7fo9Pp/mju0J83kzvLm0Bsayu7OQSLEoJxLDCBjIIyb87bkzufoOvdDxLkSlMfkF7UxomC4+eztUBSA==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.6.tgz", + "integrity": "sha512-uZnh24On2bk478AkaDAKcpRiYhS3qFSaSNvpXxV6/Ek/BBDICkWGG7MnBSfjnIlCVBei5vsLlIeQYxbF22+Udg==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=7" + "eslint": "^9 || ^10" } }, "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "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": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" - } - }, - "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" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "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": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/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/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "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.9.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2221,16 +2093,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/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/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -2244,7 +2106,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -2254,16 +2116,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2281,36 +2133,6 @@ "dev": true, "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==", - "dev": true, - "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==", - "dev": true, - "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", @@ -2325,40 +2147,35 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "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", - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "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==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "flat-cache": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=16.0.0" } }, "node_modules/find-up": { @@ -2379,24 +2196,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "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" }, @@ -2436,13 +2252,6 @@ "node": ">= 6" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2467,6 +2276,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2513,28 +2332,6 @@ "node": ">= 0.4" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "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", @@ -2549,82 +2346,28 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "17.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.12.0.tgz", + "integrity": "sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==", "dev": true, "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-symbols": { @@ -2666,6 +2409,23 @@ "node": ">= 0.4" } }, + "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/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -2689,23 +2449,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2716,25 +2459,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2758,26 +2482,6 @@ "node": ">=0.10.0" } }, - "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==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2791,27 +2495,17 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, "bin": { - "js-yaml": "bin/js-yaml.js" + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/json-buffer": { @@ -2835,6 +2529,19 @@ "dev": true, "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": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2859,6 +2566,279 @@ "node": ">= 0.8.0" } }, + "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", @@ -2875,13 +2855,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2894,37 +2867,23 @@ "loose-envify": "cli.js" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "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": "MIT", - "engines": { - "node": ">= 8" + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" } }, "node_modules/mime-db": { @@ -2949,16 +2908,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.8" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -2968,9 +2930,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2993,12 +2955,15 @@ "dev": true, "license": "MIT" }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/object-assign": { "version": "4.1.1", @@ -3009,16 +2974,6 @@ "node": ">=0.10.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3069,19 +3024,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3092,16 +3034,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3112,16 +3044,6 @@ "node": ">=8" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3130,22 +3052,22 @@ "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==", + "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": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -3163,7 +3085,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3198,32 +3120,38 @@ } }, "node_modules/postcss-mixins": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-9.0.4.tgz", - "integrity": "sha512-XVq5jwQJDRu5M1XGkdpgASqLk37OqkH4JCFDXl/Dn7janOJjCTEKL+36cnRVy7bMtoBzALfO7bV7nTIsFnUWLA==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/postcss-mixins/-/postcss-mixins-12.1.2.tgz", + "integrity": "sha512-90pSxmZVfbX9e5xCv7tI5RV1mnjdf16y89CJKbf/hD7GyOz1FCxcYMl8ZYA8Hc56dbApTKKmU9HfvgfWdCxlwg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "fast-glob": "^3.2.11", - "postcss-js": "^4.0.0", - "postcss-simple-vars": "^7.0.0", - "sugarss": "^4.0.1" + "postcss-js": "^4.0.1", + "postcss-simple-vars": "^7.0.1", + "sugarss": "^5.0.0", + "tinyglobby": "^0.2.14" }, "engines": { - "node": ">=14.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": "^20.0 || ^22.0 || >=24.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", "dev": true, "funding": [ { @@ -3237,33 +3165,33 @@ ], "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.1.1" + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">=12.0" + "node": ">=18.0" }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-preset-mantine": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.6.0.tgz", - "integrity": "sha512-1HB4suDbgvFGIuzSEyz2rMelrsw1iIw2ityVtnkzVkLlePPnn670KsoHRD0r5g9+5u/xLapoQz0Rh8AVOR4nnQ==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/postcss-preset-mantine/-/postcss-preset-mantine-1.18.0.tgz", + "integrity": "sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA==", "dev": true, "license": "MIT", "dependencies": { - "postcss-mixins": "^9.0.4", - "postcss-nested": "^6.0.1" + "postcss-mixins": "^12.0.0", + "postcss-nested": "^7.0.2" }, "peerDependencies": { "postcss": ">=8.0.0" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", - "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz", + "integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -3344,27 +3272,6 @@ "node": ">=6" } }, - "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==", - "dev": true, - "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/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -3454,35 +3361,41 @@ } }, "node_modules/react-router": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", - "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.3.tgz", + "integrity": "sha512-gyXgtdr5uACJ5b1Q4udzjVV+tb/rlHIMJKuJ0e89R4Kzgz47z/rgP0dIKxktqIEUhDHluGTPJJH/wRha7CyqsA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.3" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.30.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", - "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.3.tgz", + "integrity": "sha512-ytVbyBBM7vMfRCam25r0WMhSVSom909A8p+8m0/f1w853dz/xfFu6etAT2SEbVoSnI+ZoPRDqIsQXVT89gp7kg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.3", - "react-router": "6.30.4" + "react-router": "7.18.3" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/react-style-singleton": { @@ -3524,111 +3437,38 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.62.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", - "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "node_modules/rolldown": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", + "integrity": "sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.148.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.62.2", - "@rollup/rollup-android-arm64": "4.62.2", - "@rollup/rollup-darwin-arm64": "4.62.2", - "@rollup/rollup-darwin-x64": "4.62.2", - "@rollup/rollup-freebsd-arm64": "4.62.2", - "@rollup/rollup-freebsd-x64": "4.62.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", - "@rollup/rollup-linux-arm-musleabihf": "4.62.2", - "@rollup/rollup-linux-arm64-gnu": "4.62.2", - "@rollup/rollup-linux-arm64-musl": "4.62.2", - "@rollup/rollup-linux-loong64-gnu": "4.62.2", - "@rollup/rollup-linux-loong64-musl": "4.62.2", - "@rollup/rollup-linux-ppc64-gnu": "4.62.2", - "@rollup/rollup-linux-ppc64-musl": "4.62.2", - "@rollup/rollup-linux-riscv64-gnu": "4.62.2", - "@rollup/rollup-linux-riscv64-musl": "4.62.2", - "@rollup/rollup-linux-s390x-gnu": "4.62.2", - "@rollup/rollup-linux-x64-gnu": "4.62.2", - "@rollup/rollup-linux-x64-musl": "4.62.2", - "@rollup/rollup-openbsd-x64": "4.62.2", - "@rollup/rollup-openharmony-arm64": "4.62.2", - "@rollup/rollup-win32-arm64-msvc": "4.62.2", - "@rollup/rollup-win32-ia32-msvc": "4.62.2", - "@rollup/rollup-win32-x64-gnu": "4.62.2", - "@rollup/rollup-win32-x64-msvc": "4.62.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/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==", - "dev": true, - "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" + "@rolldown/binding-android-arm-eabi": "1.2.7", + "@rolldown/binding-android-arm64": "1.2.7", + "@rolldown/binding-darwin-arm64": "1.2.7", + "@rolldown/binding-darwin-x64": "1.2.7", + "@rolldown/binding-freebsd-x64": "1.2.7", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.7", + "@rolldown/binding-linux-arm64-gnu": "1.2.7", + "@rolldown/binding-linux-arm64-musl": "1.2.7", + "@rolldown/binding-linux-ppc64-gnu": "1.2.7", + "@rolldown/binding-linux-s390x-gnu": "1.2.7", + "@rolldown/binding-linux-x64-gnu": "1.2.7", + "@rolldown/binding-linux-x64-musl": "1.2.7", + "@rolldown/binding-openharmony-arm64": "1.2.7", + "@rolldown/binding-win32-arm64-msvc": "1.2.7", + "@rolldown/binding-win32-x64-msvc": "1.2.7" } }, "node_modules/scheduler": { @@ -3641,18 +3481,21 @@ } }, "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -3676,16 +3519,6 @@ "node": ">=8" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3696,115 +3529,69 @@ "node": ">=0.10.0" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/sugarss": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", - "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", + "integrity": "sha512-ctS5RYCBVvPoZAnzIaX5QSShK8ZiZxD5HUqSxlusvEMC+QZQIPCPOIJg6aceFX+K2rf4+SH89eu++h1Zmsr2nw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { "postcss": "^8.3.3" } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", "license": "MIT" }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "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==", + "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": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "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", - "dependencies": { - "tslib": "^1.8.1" - }, "engines": { - "node": ">= 6" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "typescript": ">=4.8.4" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/type-check": { @@ -3846,6 +3633,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.69.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.69.0.tgz", + "integrity": "sha512-B3MltX0VqjUBNEe3b3sSuiRbfa6XrfHFtBiPamjT5AsW/dfq+y+bc0wyuS9DxAS1LyzCxRp2+rxzpLUvqM2BvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.69.0", + "@typescript-eslint/parser": "8.69.0", + "@typescript-eslint/typescript-estree": "8.69.0", + "@typescript-eslint/utils": "8.69.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/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -3853,6 +3664,37 @@ "dev": true, "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "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", @@ -3959,21 +3801,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -3982,23 +3826,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.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 }, - "less": { + "@vitejs/devtools": { "optional": true }, - "lightningcss": { + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -4015,6 +3869,12 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, @@ -4044,10 +3904,10 @@ "node": ">=0.10.0" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "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" }, @@ -4063,6 +3923,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz", + "integrity": "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA==", + "dev": true, + "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" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 15abc84..5c806b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint . --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, "dependencies": { @@ -17,22 +17,23 @@ "prism-react-renderer": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.18.0" + "react-router-dom": "^7.18.3" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/node": "^20.8.0", "@types/react": "^18.3.31", "@types/react-dom": "^18.0.11", - "@typescript-eslint/eslint-plugin": "^5.57.1", - "@typescript-eslint/parser": "^5.57.1", - "@vitejs/plugin-react-swc": "^3.7.2", - "eslint": "^8.38.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "postcss": "^8.4.24", - "postcss-preset-mantine": "1.6.0", + "@vitejs/plugin-react-swc": "^4.3.3", + "eslint": "^10.9.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.6", + "globals": "^17.12.0", + "postcss": "^8.5.26", + "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", "typescript": "^5.0.2", - "vite": "^5.4.11" + "typescript-eslint": "^8.69.0", + "vite": "^8.2.2" } } diff --git a/frontend/src/components/MainSection/index.tsx b/frontend/src/components/MainSection/index.tsx index 146bf4a..4cf188c 100644 --- a/frontend/src/components/MainSection/index.tsx +++ b/frontend/src/components/MainSection/index.tsx @@ -8,7 +8,7 @@ import { TextInput, Title, } from "@mantine/core"; -import { getHotkeyHandler, useHotkeys } from "@mantine/hooks"; +import { useHotkeys } from "@mantine/hooks"; import { IconAlertTriangle, IconBolt, @@ -158,7 +158,13 @@ export default function Main() { rightSectionWidth="6rem" value={query} onChange={handleChange} - onKeyDown={getHotkeyHandler([["Enter", () => runSearch(query)]])} + // A plain handler rather than Mantine's getHotkeyHandler, which + // builds its handler during render and closes over runSearch, + // which touches a ref. Identical behaviour for a single + // unmodified key. + onKeyDown={(event) => { + if (event.key === "Enter") runSearch(query); + }} classNames={{ input: classes.input }} autoFocus /> diff --git a/frontend/src/hooks/useTypewriter.ts b/frontend/src/hooks/useTypewriter.ts index 87b9a23..4dfd551 100644 --- a/frontend/src/hooks/useTypewriter.ts +++ b/frontend/src/hooks/useTypewriter.ts @@ -3,34 +3,40 @@ import { useEffect, useState } from "react"; /** * Cycles a placeholder-style string through a list of phrases, typing each * out character by character then pausing before the next one. + * + * The visible string is derived from a character count rather than held in its + * own state. The earlier version called setDisplayed("") synchronously inside + * the effect to clear the previous phrase, which React 19's lint rules flag as + * a cascading render: the effect ran, set state, and forced a second render + * before the browser painted. Deriving it means the reset happens in the same + * update that advances the phrase, so there is no intermediate render and no + * flicker between phrases. */ export function useTypewriter(phrases: string[], enabled = true): string { const [index, setIndex] = useState(0); - const [displayed, setDisplayed] = useState(""); + const [charCount, setCharCount] = useState(0); + const phrase = phrases[index] ?? ""; useEffect(() => { if (!enabled) return; - const phrase = phrases[index]; - let charIndex = 0; - setDisplayed(""); const typeInterval = setInterval(() => { - charIndex += 1; - setDisplayed(phrase.slice(0, charIndex)); - if (charIndex >= phrase.length) { - clearInterval(typeInterval); - } + setCharCount((count) => (count >= phrase.length ? count : count + 1)); }, 55); - const nextPhraseTimeout = setTimeout(() => { - setIndex((i) => (i + 1) % phrases.length); - }, phrase.length * 55 + 2400); + const nextPhraseTimeout = setTimeout( + () => { + setIndex((i) => (i + 1) % phrases.length); + setCharCount(0); + }, + phrase.length * 55 + 2400, + ); return () => { clearInterval(typeInterval); clearTimeout(nextPhraseTimeout); }; - }, [index, phrases, enabled]); + }, [phrase, phrases.length, enabled]); - return displayed; + return phrase.slice(0, charCount); } diff --git a/package.json.frontend.bak b/package.json.frontend.bak new file mode 100644 index 0000000..15abc84 --- /dev/null +++ b/package.json.frontend.bak @@ -0,0 +1,38 @@ +{ + "name": "frontend_v2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "@mantine/core": "7.1.0", + "@mantine/hooks": "7.1.0", + "@tabler/icons-react": "^2.35.0", + "axios": "^1.5.1", + "prism-react-renderer": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.18.0" + }, + "devDependencies": { + "@types/node": "^20.8.0", + "@types/react": "^18.3.31", + "@types/react-dom": "^18.0.11", + "@typescript-eslint/eslint-plugin": "^5.57.1", + "@typescript-eslint/parser": "^5.57.1", + "@vitejs/plugin-react-swc": "^3.7.2", + "eslint": "^8.38.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.3.4", + "postcss": "^8.4.24", + "postcss-preset-mantine": "1.6.0", + "postcss-simple-vars": "^7.0.1", + "typescript": "^5.0.2", + "vite": "^5.4.11" + } +}