diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f0de95a..c7e15210 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,39 +2,74 @@ name: CI on: pull_request: - branches: [ main ] + branches: [main] push: - branches: [ main ] + branches: [main] jobs: - ci: + # Build native module on Linux (fast sanity check) + build-native: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup pnpm - uses: pnpm/action-setup@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: osgrep-core + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Build native module + working-directory: osgrep-core + run: napi build --platform --release + + - name: Upload artifact + uses: actions/upload-artifact@v4 with: - version: 9 - run_install: false + name: native-linux + path: osgrep-core/*.node + + # TypeScript checks and tests + ci: + runs-on: ubuntu-latest + needs: build-native + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - cache: 'pnpm' - cache-dependency-path: 'pnpm-lock.yaml' + + - name: Download native module + uses: actions/download-artifact@v4 + with: + name: native-linux + path: osgrep-core - name: Install dependencies - run: pnpm install --frozen-lockfile + run: bun install - name: Check types - run: pnpm run typecheck + run: bun run typecheck - name: Run tests - run: pnpm run test + run: bun run test - name: Build - run: pnpm run build - + run: bun run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66336a7c..7343b46c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,34 +6,106 @@ on: - 'v*.*.*' workflow_dispatch: +env: + CARGO_INCREMENTAL: 0 + jobs: - publish-npm: + # ============================================================================= + # Build native binaries for each platform + # ============================================================================= + build-native: + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-unknown-linux-musl + os: ubuntu-latest + use-cross: true + - target: x86_64-pc-windows-msvc + os: windows-latest + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install dependencies (Linux musl) + if: matrix.use-cross + run: | + sudo apt-get update + sudo apt-get install -y musl-tools + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Build native module + working-directory: osgrep-core + run: napi build --platform --release --target ${{ matrix.target }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: bindings-${{ matrix.target }} + path: osgrep-core/*.node + if-no-files-found: error + + # ============================================================================= + # Publish platform packages + meta package + osgrep + # ============================================================================= + publish: runs-on: ubuntu-latest + needs: build-native permissions: contents: write id-token: write + steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - run_install: false - - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' registry-url: 'https://registry.npmjs.org' - cache: 'pnpm' - cache-dependency-path: 'pnpm-lock.yaml' + + - name: Install napi-rs CLI + run: npm install -g @napi-rs/cli + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: osgrep-core/artifacts + + - name: Move artifacts to osgrep-core + run: | + cd osgrep-core + for dir in artifacts/bindings-*/; do + mv "$dir"*.node . 2>/dev/null || true + done + rm -rf artifacts + ls -la *.node - name: Verify tag commit is on main - working-directory: . run: | git fetch origin main --depth=1 if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then @@ -41,12 +113,6 @@ jobs: exit 1 fi - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Check types - run: pnpm run typecheck - - name: Verify tag matches package.json version run: | TAG="${GITHUB_REF##*/}" @@ -56,15 +122,36 @@ jobs: exit 1 fi - - name: Build - run: pnpm build + - name: Publish osgrep-core platform packages + working-directory: osgrep-core + run: napi prepublish -t npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish osgrep-core meta package + working-directory: osgrep-core + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Install bun + uses: oven-sh/setup-bun@v2 + + - name: Install osgrep dependencies + run: bun install + + - name: Check types + run: bun run typecheck + + - name: Build osgrep + run: bun run build - - name: Publish to npm - run: pnpm publish --access public --provenance --no-git-checks + - name: Publish osgrep to npm + run: npm publish --access public --provenance env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Create GitHub Release (tag only, no assets) + - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 9b85d444..e08a9c8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,53 @@ dist/ node_modules/ +**/node_modules/ *.tsbuildinfo *.tgz +.DS_Store + +# Local osgrep data +.osgrep/ + +# Development files +CLAUDE.md +TODO.md +AGENTS.md ADVANCED.md -test-build.sh -TEST_SUITE_PROMPT.md -WATCH_MODE_PROMPT.md ENGINEERING_DIARY.md RELEASE_GUIDE.md -osgrep/run-benchmark.sh +TEST_SUITE_PROMPT.md +WATCH_MODE_PROMPT.md +AGENT_BENCHMARK.md +latest_review.md +plan.md + +# Benchmark & experiments (dev-only) +benchmark/ +experiments/ +scripts/ +tools/ +public/ +test_skeleton.py +test-build.sh run-agent-benchmark.sh setup-benchmark-repos.sh -AGENT_BENCHMARK.md benchmark-results.json -src/bench/benchmark-agent.ts -src/bench/generate-benchmark-chart.ts -src/.env.local -CLAUDE.md -.DS_Store -latest_review.md benchmarks/*.log -.osgrep/server.json -.osgrep/cache/meta.lmdb-lock +src/bench/ + +# Env files +src/.env.local +.env* +# Lockfiles (using bun) +pnpm-lock.yaml +package-lock.json +yarn.lock + +bun.lockb +# Native core (Rust / N-API) build outputs +osgrep-core/target/ +**/target/ +osgrep-core/*.node +osgrep-core/bench/ .osgrep -TODO.md -AGENTS.md \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 6d69c9c6..00000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2025 Mixedbread AI (original mgrep work) - Copyright 2025 Ryan D'Onofrio (osgrep modifications and enhancements) - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 62b2f374..00000000 --- a/NOTICE +++ /dev/null @@ -1,33 +0,0 @@ -osgrep -Copyright 2025 Ryan Donofrio - -This product includes software originally developed by MixedBread as mgrep. -Original mgrep Copyright 2025 MixedBread -https://github.com/mixedbread-ai/mgrep - -================================================================================ - -MODIFICATIONS AND ENHANCEMENTS - -osgrep is a substantial modification and enhancement of the original mgrep -codebase. The following major changes and additions have been made by -Ryan Donofrio: - -- Complete rewrite to use local-only embeddings (removing remote API dependencies) -- Integration with @huggingface/transformers for local model inference -- Implementation of LanceDB for local vector storage -- New local storage architecture and indexing system -- Enhanced chunking and context management -- Addition of watch mode for real-time index updates -- New CLI commands and improved user experience -- Comprehensive benchmarking and evaluation tools -- Extensive test suite additions -- Documentation and setup improvements - - -================================================================================ - -THIRD-PARTY DEPENDENCIES - -This software depends on various open-source libraries. See package.json for -a complete list of dependencies and their respective licenses. diff --git a/README.md b/README.md index e88861c5..47008e29 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,10 @@ Natural-language search that works like `grep`. Fast, local, and built for coding agents. - **Semantic:** Finds concepts ("where do transactions get created?"), not just strings. -- **Call Graph Tracing:** Map dependencies with `trace` to see who calls what. - **Role Detection:** Distinguishes `ORCHESTRATION` (high-level logic) from `DEFINITION` (types/classes). -- **Local & Private:** 100% local embeddings via `onnxruntime-node`. +- **Local & Private:** 100% local embeddings via `onnxruntime`. - **Auto-Isolated:** Each repository gets its own index automatically. -- **Agent-Ready:** Native output with symbols, roles, and call graphs. +- **Agent-Ready:** Native output with symbols and roles. ## Quick Start @@ -50,26 +49,6 @@ Natural-language search that works like `grep`. Fast, local, and built for codin **Your first search will automatically index the repository.** Each repository is automatically isolated with its own index. Switching between repos "just works" — no manual configuration needed. If the background server is running (`osgrep serve`), search goes through the hot daemon; otherwise it falls back to on-demand indexing. -4. **Trace** (Call Graph) - - ```bash - osgrep trace "function_name" - ``` -See who calls a function (upstream dependencies) and what it calls (downstream dependencies). Perfect for impact analysis and understanding code flow. - -To find the symbols in your code base: - ```bash - osgrep symbols - ``` - -In our public benchmarks, `osgrep` can save about 20% of your LLM tokens and deliver a 30% speedup. - -
- osgrep benchmark -
- - - ### Claude Code Plugin 1. Run `osgrep install-claude-code` @@ -104,15 +83,20 @@ osgrep "how is the database connection pooled?" | `--scores` | Show relevance scores (0-1) for each result. | `false` | | `--min-score ` | Filter out results below this score threshold. | `0` | | `--compact` | Show file paths only (like `grep -l`). | `false` | +| `--deep` | Include related code (callers, definitions) for architectural context. | `false` | | `-s`, `--sync` | Force re-index changed files before searching. | `false` | | `-r`, `--reset` | Reset the index and re-index from scratch. | `false` | + **Examples:** ```bash # General concept search osgrep "API rate limiting logic" -# Deep dive (show more matches per file) +# Deep dive with architectural context (shows callers and definitions) +osgrep "how does authentication work" --deep + +# Show more matches per file osgrep "error handling" --per-file 5 # Just give me the files @@ -158,7 +142,7 @@ Runs a lightweight HTTP server with live file watching so searches stay hot in R - Keeps LanceDB and the embedding worker resident for <50ms responses. - Watches the repo (via chokidar) and incrementally re-indexes on change. - Health endpoint: `GET /health` -- Search endpoint: `POST /search` with `{ query, limit, path, rerank }` +- Search endpoint: `POST /search` with `{ query, limit, path, deep }` - Writes lock: `.osgrep/server.json` with `port`/`pid` **Options:** @@ -205,41 +189,68 @@ osgrep serve stop --all Claude Code hooks start/stop this automatically; you rarely need to run it manually. -### `osgrep list` +### `osgrep trace` -Lists all indexed repositories (stores) and their metadata. +Shows callers and callees for a symbol in one shot — replaces multiple search/read cycles when tracing code flow. ```bash -osgrep list +osgrep trace handleRequest ``` -Shows store names, sizes, and last modified times. Useful for seeing what's indexed and cleaning up old stores. +**Output (default plain format):** +``` +handleRequest + def: src/server/handler.ts:45 + calls: validateAuth routeRequest sendResponse + called_by: src/index.ts:12 src/api/router.ts:87 +``` -### `osgrep skeleton` +**Options:** + +| Flag | Description | Default | +| --- | --- | --- | +| `--depth ` | Traversal depth for nested calls | `1` | +| `--callers` | Show only callers (who calls this) | `false` | +| `--callees` | Show only callees (what this calls) | `false` | +| `--path ` | Filter results to path prefix | - | +| `--pretty` | Tree view output for humans | `false` | +| `--json` | JSON output for programmatic use | `false` | -Generates a compressed "skeleton" of a file, showing only signatures, types, and class structures while eliding function bodies. +**Examples:** ```bash -osgrep skeleton src/lib/auth.ts +# Basic trace +osgrep trace Searcher + +# Only show what calls this function +osgrep trace handleAuth --callers + +# Only show what this function calls +osgrep trace handleAuth --callees + +# Filter to specific directory +osgrep trace validateToken --path src/auth + +# Pretty tree output +osgrep trace processRequest --pretty ``` -**Output:** -```typescript -class AuthService { - validate(token: string): boolean { - // → jwt.verify, checkScope, .. | C:5 | ORCH - } -} +**Server endpoint:** +```bash +curl -X POST http://localhost:4444/trace \ + -H "Content-Type: application/json" \ + -d '{"symbol": "Searcher", "depth": 1, "callers": true}' ``` -**Modes:** -- `osgrep skeleton `: Skeletonize specific file. -- `osgrep skeleton `: Find symbol in index and skeletonize its file. -- `osgrep skeleton "query"`: Search for query and skeletonize top matches. +### `osgrep list` -**Supported Languages:** -TypeScript, JavaScript, Python, Go, Rust, Java, C#, C++, C, Ruby, PHP. +Lists all indexed repositories (stores) and their metadata. + +```bash +osgrep list +``` +Shows store names, sizes, and last modified times. Useful for seeing what's indexed and cleaning up old stores. ### `osgrep doctor` @@ -303,9 +314,9 @@ osgrep respects both `.gitignore` and `.osgrepignore` files when indexing. Creat ## Development ```bash -pnpm install -pnpm build # or pnpm dev -pnpm format # biome check +bun install +bun run build +bun run format # biome check ``` ## Troubleshooting @@ -316,15 +327,7 @@ pnpm format # biome check - **Want faster indexing?** Keep fallback disabled (default) to skip files without TreeSitter support. - **Need a fresh start?** Delete `~/.osgrep/data` and `~/.osgrep/meta.json` and run `osgrep index`. -## Attribution - -osgrep is built upon the foundation of [mgrep](https://github.com/mixedbread-ai/mgrep) by MixedBread. We acknowledge and appreciate the original architectural concepts and design decisions that informed this work. - - -See the [NOTICE](NOTICE) file for detailed attribution information. - ## License -Licensed under the Apache License, Version 2.0. -See [LICENSE](LICENSE) and [Apache-2.0](https://opensource.org/licenses/Apache-2.0) for details. +Licensed under the [Apache License, Version 2.0](https://opensource.org/licenses/Apache-2.0). diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000..169254e1 Binary files /dev/null and b/bun.lockb differ diff --git a/experiments/mrr-sweep.ts b/experiments/mrr-sweep.ts deleted file mode 100644 index 063b601e..00000000 --- a/experiments/mrr-sweep.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { performance } from "node:perf_hooks"; -import { cases, evaluateCase } from "../src/eval"; -import { Searcher } from "../src/lib/search/searcher"; -import type { SearchFilter } from "../src/lib/store/types"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; -import { destroyWorkerPool } from "../src/lib/workers/pool"; - -type Scenario = { - name: string; - env: Record; - searchOptions?: { rerank?: boolean }; -}; - -const enableDiagnostics = - process.env.OSGREP_SWEEP_DEBUG === "1" || - process.env.OSGREP_SWEEP_DEBUG === "true"; - -type Ranked = { path: string; score?: number }; - -function topPaths(response: { data: any[] }, k = 3): Ranked[] { - return (response.data || []).slice(0, k).map((chunk) => ({ - path: chunk?.metadata?.path || "", - score: chunk?.score ?? chunk?.generated_metadata?._score, - })); -} - -function targetRank(response: { data: any[] }, expectedPath: string): number { - const expectedPaths = expectedPath - .split("|") - .map((p) => p.trim().toLowerCase()) - .filter(Boolean); - return response.data.findIndex((chunk: any) => { - const path = chunk?.metadata?.path?.toLowerCase?.() || ""; - return expectedPaths.some((expected) => path.includes(expected)); - }); -} - -// Keep runs consistent and serial. -process.env.OSGREP_WORKER_THREADS ??= "1"; -process.env.OSGREP_WORKER_COUNT ??= "1"; - -const scenarios: Scenario[] = [ - { - name: "baseline-rerank-on", - env: {}, - searchOptions: { rerank: true }, - }, - { - name: "oldish-no-rerank-strong-boosts", - env: { - OSGREP_CODE_BOOST: "1.25", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.5", - OSGREP_PRE_K: "300", - OSGREP_STAGE1_K: "400", - OSGREP_STAGE2_K: "0", // skip pooled filter - OSGREP_MAX_PER_FILE: "50", - }, - searchOptions: { rerank: false }, - }, - { - name: "small-rerank-strong-boosts", - env: { - OSGREP_CODE_BOOST: "1.25", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.5", - OSGREP_PRE_K: "300", - OSGREP_STAGE1_K: "400", - OSGREP_STAGE2_K: "80", - OSGREP_MAX_PER_FILE: "50", - }, - searchOptions: { rerank: true }, - }, - { - name: "no-diversify-rerank", - env: { - OSGREP_CODE_BOOST: "1.2", - OSGREP_TEST_PENALTY: "0.85", - OSGREP_DOC_PENALTY: "0.7", - OSGREP_PRE_K: "400", - OSGREP_STAGE1_K: "500", - OSGREP_STAGE2_K: "120", - OSGREP_MAX_PER_FILE: "1000", - }, - searchOptions: { rerank: true }, - }, -]; - -async function runScenario(scenario: Scenario) { - const touched: Record = {}; - for (const [key, value] of Object.entries(scenario.env)) { - touched[key] = process.env[key]; - process.env[key] = value; - } - - const searchRoot = process.cwd(); - const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; - const paths = ensureProjectPaths(projectRoot); - const vectorDb = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(vectorDb); - - const results: ReturnType[] = []; - let drops = 0; - let lifts = 0; - let total = 0; - let better = 0; - let worse = 0; - let unchanged = 0; - - for (const c of cases) { - const queryStart = performance.now(); - let fusedTop: Ranked[] = []; - let rerankTop: Ranked[] = []; - let fusedRank = -1; - - if (enableDiagnostics && scenario.searchOptions?.rerank !== false) { - const fusedOnly = await searcher.search( - c.query, - 20, - { rerank: false }, - undefined as SearchFilter | undefined, - ); - fusedTop = topPaths(fusedOnly); - fusedRank = targetRank(fusedOnly, c.expectedPath); - } - - const res = await searcher.search(c.query, 20, scenario.searchOptions); - rerankTop = topPaths(res); - const rerankRank = enableDiagnostics ? targetRank(res, c.expectedPath) : -1; - - if (enableDiagnostics && fusedTop.length && rerankTop.length) { - total += 1; - const fusedBest = fusedTop[0]?.path?.toLowerCase?.() || ""; - const rerankFirstThree = rerankTop - .slice(0, 3) - .map((r) => r.path?.toLowerCase?.() || ""); - if (fusedBest && !rerankFirstThree.some((p) => p.includes(fusedBest))) { - drops += 1; - console.log( - `[debug] ${c.query}\n fused#1: ${fusedTop - .map((r) => r.path) - .join(", ")}\n rerank#3: ${rerankTop - .map((r) => r.path) - .join(", ")}`, - ); - } - - const rerankBest = rerankTop[0]?.path?.toLowerCase?.() || ""; - const fusedFirstThree = fusedTop - .slice(0, 3) - .map((r) => r.path?.toLowerCase?.() || ""); - if ( - rerankBest && - !fusedFirstThree.some((p) => p.includes(rerankBest)) && - fusedTop.some((r) => - (r.path?.toLowerCase?.() || "").includes(rerankBest), - ) - ) { - lifts += 1; - console.log( - `[lift] ${c.query}\n fused#3: ${fusedTop - .map((r) => r.path) - .join(", ")}\n rerank#1: ${rerankTop - .map((r) => r.path) - .join(", ")}`, - ); - } - - if (fusedRank >= 0 && rerankRank >= 0) { - if (rerankRank < fusedRank) { - better += 1; - } else if (rerankRank > fusedRank) { - worse += 1; - console.log( - `[worse] ${c.query} fusedRank=${fusedRank + 1} rerankRank=${ - rerankRank + 1 - } path=${c.expectedPath}`, - ); - } else { - unchanged += 1; - } - } - } - - const timeMs = performance.now() - queryStart; - results.push(evaluateCase(res, c, timeMs)); - } - - const mrr = results.reduce((sum, r) => sum + r.rr, 0) / results.length; - const recallAt10 = - results.reduce((sum, r) => sum + r.recall, 0) / results.length; - const avgTime = - results.reduce((sum, r) => sum + r.timeMs, 0) / results.length; - const found = results.filter((r) => r.found).length; - - console.log(`\n=== ${scenario.name} ===`); - console.log( - `MRR: ${mrr.toFixed(3)} | Recall@10: ${recallAt10.toFixed(3)} | Avg ms: ${avgTime.toFixed(0)} | Found: ${found}/${results.length}`, - ); - if (enableDiagnostics && total > 0) { - console.log( - `Rerank dropped fused#1 out of top3 for ${drops}/${total} queries; promoted new #1 outside fused top3 for ${lifts}/${total} queries`, - ); - if (better + worse + unchanged > 0) { - console.log( - `Target rank delta: better ${better}, worse ${worse}, unchanged ${unchanged} (out of ${better + worse + unchanged})`, - ); - } - } - - // Restore env (best-effort) so each scenario is clean. - for (const [key, prev] of Object.entries(touched)) { - if (prev === undefined) delete process.env[key]; - else process.env[key] = prev; - } -} - -async function main() { - const filter = process.argv[2]; - const runList = filter - ? scenarios.filter((s) => s.name === filter) - : scenarios; - - for (const scenario of runList) { - try { - await runScenario(scenario); - } catch (err) { - console.error(`Scenario ${scenario.name} failed:`, err); - } - } - - try { - await new Promise((resolve) => setTimeout(resolve, 200)); - await destroyWorkerPool(); - } catch { - // Swallow cleanup errors for experiments. - } -} - -main().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/experiments/quick_check.ts b/experiments/quick_check.ts deleted file mode 100644 index 5274985e..00000000 --- a/experiments/quick_check.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; - -async function main() { - const root = process.cwd(); - const paths = ensureProjectPaths(findProjectRoot(root) ?? root); - const db = new VectorDB(paths.lancedbDir); - const table = await db.ensureTable(); - const rows = (await table.query().limit(5).toArray()) as any[]; - rows.forEach((r, i) => { - const col = r.colbert; - let len = 0; - if (Buffer.isBuffer(col)) len = col.length; - else if (Array.isArray(col)) len = col.length; - else if ( - col && - typeof col === "object" && - "length" in (col as Record) - ) - len = Number((col as { length: number }).length) || 0; - else if (col && col.type === "Buffer" && Array.isArray(col.data)) - len = col.data.length; - console.log( - `#${i} path=${r.path}, colbertLen=${len}, scale=${r.colbert_scale}`, - ); - }); -} -main().catch(console.error); diff --git a/experiments/ranking-test.ts b/experiments/ranking-test.ts deleted file mode 100644 index 6c4f7b48..00000000 --- a/experiments/ranking-test.ts +++ /dev/null @@ -1,46 +0,0 @@ -// experiments/ranking-test.ts - -import { Searcher } from "../src/lib/search/searcher"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { ensureProjectPaths } from "../src/lib/utils/project-root"; - -async function run() { - const root = process.cwd(); - const paths = ensureProjectPaths(root); - const db = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(db); - - const cases = [ - { query: "Request Validation", expected: "request_body_to_args" }, - { query: "Dependency Injection", expected: "solve_dependencies" }, - ]; - - for (const c of cases) { - console.log(`\nšŸ”Ž Query: "${c.query}"`); - const results = await searcher.search(c.query, 10, { rerank: true }); - - // Filter out this test file - const filtered = results.data.filter( - (r) => !r.metadata?.path.includes("ranking-test.ts"), - ); - - const found = filtered.findIndex( - (r) => - r.metadata?.path.includes(c.expected) || r.text?.includes(c.expected), - ); - - if (found === 0) console.log(`āœ… PASS: Found '${c.expected}' at rank #1`); - else if (found > 0) - console.log(`āš ļø WARN: Found '${c.expected}' at rank #${found + 1}`); - else console.log(`āŒ FAIL: Did not find '${c.expected}' in top results`); - - // Debug: Show top 3 roles/scores - filtered.slice(0, 3).forEach((r, i) => { - console.log( - ` ${i + 1}. [${r.role ?? "UNK"}] ${r.metadata?.path} (Score: ${r.score.toFixed(3)})`, - ); - }); - } -} - -run(); diff --git a/experiments/verify-fix.ts b/experiments/verify-fix.ts deleted file mode 100644 index c4773ac9..00000000 --- a/experiments/verify-fix.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { performance } from "node:perf_hooks"; -import { Searcher } from "../src/lib/search/searcher"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { - ensureProjectPaths, - findProjectRoot, -} from "../src/lib/utils/project-root"; - -// Force old-engine style boosts for this check. -process.env.OSGREP_CODE_BOOST = "1.25"; -process.env.OSGREP_TEST_PENALTY = "0.85"; -process.env.OSGREP_DOC_PENALTY = "0.5"; - -async function main() { - const root = process.cwd(); - const projectRoot = findProjectRoot(root) ?? root; - const paths = ensureProjectPaths(projectRoot); - const db = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(db); - - const warmStart = performance.now(); - await searcher.search("test query", 1, { rerank: true }); - console.log( - `Warmup Query Time: ${(performance.now() - warmStart).toFixed(0)}ms`, - ); - - const res = await searcher.search("TreeSitterParser", 5, { rerank: true }); - console.log("\nContext Visibility Check:"); - res.data.forEach((r, i) => { - const path = r.metadata?.path ?? ""; - const score = typeof r.score === "number" ? r.score.toFixed(3) : "n/a"; - console.log(`${i + 1}. ${path} (Score: ${score})`); - if (!r.text?.includes("TreeSitterParser")) { - console.log(" (Matched via vectorized context, not literal text)"); - } - }); -} - -main().catch((err) => { - console.error(err); - process.exitCode = 1; -}); diff --git a/osgrep-core/.gitignore b/osgrep-core/.gitignore new file mode 100644 index 00000000..0b03a24c --- /dev/null +++ b/osgrep-core/.gitignore @@ -0,0 +1,5 @@ +target/ +node_modules/ +*.node +.DS_Store +bench/opencode/ diff --git a/osgrep-core/Cargo.lock b/osgrep-core/Cargo.lock new file mode 100644 index 00000000..cda7c17d --- /dev/null +++ b/osgrep-core/Cargo.lock @@ -0,0 +1,2754 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compact_str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dary_heap" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" +dependencies = [ + "serde", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "esaxx-rs" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" +dependencies = [ + "cc", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hf-hub" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629d8f3bbeda9d148036d6b0de0a3ab947abd08ce90626327fc3547a49d59d97" +dependencies = [ + "dirs", + "futures", + "http", + "indicatif", + "libc", + "log", + "native-tls", + "num_cpus", + "rand", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "ureq 2.12.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec 1.15.1", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec 1.15.1", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec 1.15.1", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro_rules_attribute" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +dependencies = [ + "macro_rules_attribute-proc_macro", + "paste", +] + +[[package]] +name = "macro_rules_attribute-proc_macro" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "monostate" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67" +dependencies = [ + "monostate-impl", + "serde", + "serde_core", +] + +[[package]] +name = "monostate-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi" +version = "2.16.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" +dependencies = [ + "bitflags 2.10.0", + "ctor", + "napi-derive", + "napi-sys", + "once_cell", +] + +[[package]] +name = "napi-build" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" + +[[package]] +name = "napi-derive" +version = "2.16.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbe2585d8ac223f7d34f13701434b9d5f4eb9c332cccce8dee57ea18ab8ab0c" +dependencies = [ + "cfg-if", + "convert_case", + "napi-derive-backend", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "napi-derive-backend" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1639aaa9eeb76e91c6ae66da8ce3e89e921cd3885e99ec85f4abacae72fc91bf" +dependencies = [ + "convert_case", + "once_cell", + "proc-macro2", + "quote", + "regex", + "semver", + "syn", +] + +[[package]] +name = "napi-sys" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" +dependencies = [ + "libloading", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "onig" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" +dependencies = [ + "bitflags 2.10.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f86c6eef3d6df15f23bcfb6af487cbd2fed4e5581d58d5bf1f5f8b7f6727dc" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ort" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa7e49bd669d32d7bc2a15ec540a527e7764aec722a45467814005725bcd721" +dependencies = [ + "ndarray", + "ort-sys", + "smallvec 2.0.0-alpha.10", + "tracing", +] + +[[package]] +name = "ort-sys" +version = "2.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2aba9f5c7c479925205799216e7e5d07cc1d4fa76ea8058c60a9a30f6a4e890" +dependencies = [ + "flate2", + "pkg-config", + "sha2", + "tar", + "ureq 3.1.4", +] + +[[package]] +name = "osgrep-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "hf-hub", + "napi", + "napi-build", + "napi-derive", + "once_cell", + "ort", + "serde", + "serde_json", + "tokenizers", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-cond" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f" +dependencies = [ + "either", + "itertools", + "rayon", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.12.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smallvec" +version = "2.0.0-alpha.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d44cfb396c3caf6fbfd0ab422af02631b69ddd96d2eff0b0f0724f9024051b" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + +[[package]] +name = "spm_precompiled" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326" +dependencies = [ + "base64 0.13.1", + "nom", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokenizers" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a620b996116a59e184c2fa2dfd8251ea34a36d0a514758c6f966386bd2e03476" +dependencies = [ + "ahash", + "aho-corasick", + "compact_str", + "dary_heap", + "derive_builder", + "esaxx-rs", + "getrandom 0.3.4", + "indicatif", + "itertools", + "log", + "macro_rules_attribute", + "monostate", + "onig", + "paste", + "rand", + "rayon", + "rayon-cond", + "regex", + "regex-syntax", + "serde", + "serde_json", + "spm_precompiled", + "thiserror", + "unicode-normalization-alignments", + "unicode-segmentation", + "unicode_categories", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization-alignments" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de" +dependencies = [ + "smallvec 1.15.1", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "native-tls", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +dependencies = [ + "base64 0.22.1", + "der", + "log", + "native-tls", + "percent-encoding", + "rustls-pki-types", + "socks", + "ureq-proto", + "utf-8", + "webpki-root-certs", +] + +[[package]] +name = "ureq-proto" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.4", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/osgrep-core/Cargo.toml b/osgrep-core/Cargo.toml new file mode 100644 index 00000000..228e24e7 --- /dev/null +++ b/osgrep-core/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "osgrep-core" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# NAPI-RS bindings +napi = { version = "2", default-features = false, features = ["napi4"] } +napi-derive = "2" + +# Utilities +once_cell = "1" +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# HuggingFace tokenizers + model hub +tokenizers = "0.21" +hf-hub = "0.4" + +# ONNX Runtime (the only ML backend we need) +ort = { version = "2.0.0-rc.10", default-features = true, features = ["download-binaries"] } + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true +opt-level = 3 diff --git a/osgrep-core/build.rs b/osgrep-core/build.rs new file mode 100644 index 00000000..9fc23678 --- /dev/null +++ b/osgrep-core/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/osgrep-core/index.d.ts b/osgrep-core/index.d.ts new file mode 100644 index 00000000..7b588f84 --- /dev/null +++ b/osgrep-core/index.d.ts @@ -0,0 +1,60 @@ +// TypeScript declarations for the `osgrep-core` native module (N-API). + +export interface DenseResult { + /** Flat array of embeddings [batch_size * 384] */ + embeddings: number[]; + /** Number of texts encoded */ + count: number; +} + +export interface ColbertPackedResult { + /** Packed embeddings as flat i8 array (all docs concatenated) */ + embeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering */ + tokenIds: Uint32Array | number[]; + /** Number of tokens per document */ + lengths: Uint32Array | number[]; + /** Byte offsets into embeddings for each doc */ + offsets: Uint32Array | number[]; +} + +export interface RerankResult { + /** Original indices of top-k documents */ + indices: number[]; + /** MaxSim scores for top-k documents */ + scores: number[]; +} + +export interface EmbedResult { + /** Dense embeddings [batch_size * 384] */ + dense: number[]; + /** Packed ColBERT embeddings (i8) */ + colbertEmbeddings: Int8Array | number[]; + /** Token IDs for skiplist filtering (all docs concatenated) */ + colbertTokenIds: Uint32Array | number[]; + /** Token counts per document */ + colbertLengths: Uint32Array | number[]; + /** Byte offsets per document */ + colbertOffsets: Uint32Array | number[]; +} + +export function initModels(denseRepo: string, colbertRepo: string): void; +export function isInitialized(): boolean; + +export function embedDense(texts: string[]): DenseResult; +export function embedColbertPacked(texts: string[]): ColbertPackedResult; + +/** Returns query embeddings as a flat array [seq_len * 48]. */ +export function encodeQueryColbert(query: string): Float64Array | number[]; + +export function rerankColbert( + queryEmbeddings: Float64Array | number[], + docEmbeddings: Int8Array | number[], + docTokenIds: Uint32Array | number[], + docLengths: number[] | Uint32Array, + docOffsets: number[] | Uint32Array, + candidateIndices: number[] | Uint32Array, + topK: number, +): RerankResult; + +export function embedBatch(texts: string[]): EmbedResult; diff --git a/osgrep-core/index.js b/osgrep-core/index.js new file mode 100644 index 00000000..39323283 --- /dev/null +++ b/osgrep-core/index.js @@ -0,0 +1,124 @@ +// osgrep-core: Native embedding and reranking +// Auto-loads the correct binary for the current platform + +import { createRequire } from 'module'; +import { existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; + +const require = createRequire(import.meta.url); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const { platform, arch } = process; + +let nativeBinding = null; +let loadError = null; + +function isMusl() { + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = execSync('which ldd').toString().trim(); + return require('fs').readFileSync(lddPath, 'utf8').includes('musl'); + } catch (e) { + return true; + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header; + return !glibcVersionRuntime; + } +} + +function tryLoad(localPath, packageName) { + if (existsSync(join(__dirname, localPath))) { + return require(`./${localPath}`); + } + return require(packageName); +} + +switch (platform) { + case 'darwin': + switch (arch) { + case 'arm64': + try { + nativeBinding = tryLoad('osgrep-core.darwin-arm64.node', '@osgrep-core/darwin-arm64'); + } catch (e) { + loadError = e; + } + break; + case 'x64': + try { + nativeBinding = tryLoad('osgrep-core.darwin-x64.node', '@osgrep-core/darwin-x64'); + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`); + } + break; + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + try { + nativeBinding = tryLoad('osgrep-core.linux-x64-musl.node', '@osgrep-core/linux-x64-musl'); + } catch (e) { + loadError = e; + } + } else { + try { + nativeBinding = tryLoad('osgrep-core.linux-x64-gnu.node', '@osgrep-core/linux-x64-gnu'); + } catch (e) { + loadError = e; + } + } + break; + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`); + } + break; + case 'win32': + switch (arch) { + case 'x64': + try { + nativeBinding = tryLoad('osgrep-core.win32-x64-msvc.node', '@osgrep-core/win32-x64-msvc'); + } catch (e) { + loadError = e; + } + break; + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`); + } + break; + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); +} + +if (!nativeBinding) { + if (loadError) { + throw loadError; + } + throw new Error(`Failed to load native binding`); +} + +// Clean API exports +export const { + // Initialization + initModels, + isInitialized, + + // Dense embeddings (384-dim) + embedDense, + + // ColBERT embeddings (48-dim per token, packed) + embedColbertPacked, + + // ColBERT reranking + encodeQueryColbert, + rerankColbert, + + // Convenience: both embeddings in one call + embedBatch, +} = nativeBinding; diff --git a/osgrep-core/package.json b/osgrep-core/package.json new file mode 100644 index 00000000..69da5677 --- /dev/null +++ b/osgrep-core/package.json @@ -0,0 +1,46 @@ +{ + "name": "osgrep-core", + "version": "0.1.0", + "description": "Native Rust core for osgrep (embeddings + reranking via ONNX)", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/Ryandonofrio3/osgrep.git", + "directory": "osgrep-core" + }, + "license": "Apache-2.0", + "files": [ + "index.js", + "index.d.ts" + ], + "napi": { + "name": "osgrep-core", + "triples": { + "defaults": false, + "additional": [ + "aarch64-apple-darwin", + "x86_64-apple-darwin", + "x86_64-unknown-linux-gnu", + "x86_64-unknown-linux-musl", + "x86_64-pc-windows-msvc" + ] + } + }, + "scripts": { + "build": "napi build --platform", + "build:release": "napi build --platform --release", + "prepublishOnly": "napi prepublish -t npm" + }, + "devDependencies": { + "@napi-rs/cli": "^3.0.0-alpha.63" + }, + "optionalDependencies": { + "@osgrep-core/darwin-arm64": "0.1.0", + "@osgrep-core/darwin-x64": "0.1.0", + "@osgrep-core/linux-x64-gnu": "0.1.0", + "@osgrep-core/linux-x64-musl": "0.1.0", + "@osgrep-core/win32-x64-msvc": "0.1.0" + } +} diff --git a/osgrep-core/src/colbert_ort.rs b/osgrep-core/src/colbert_ort.rs new file mode 100644 index 00000000..1038c7bd --- /dev/null +++ b/osgrep-core/src/colbert_ort.rs @@ -0,0 +1,592 @@ +use ort::session::{Session, builder::GraphOptimizationLevel}; +use ort::value::Value; +use tokenizers::Tokenizer; +use hf_hub::{api::sync::Api, Repo, RepoType}; +use std::collections::HashSet; + +fn log_native(msg: impl AsRef) { + // Intentionally no-op: native logging was polluting CLI output. + // If you need debugging, add structured logging at the JS layer instead. + let _ = msg.as_ref(); +} + +// ColBERT special tokens (these get added during fine-tuning) +const QUERY_MARKER: &str = "[Q]"; +const DOC_MARKER: &str = "[D]"; +const QUERY_MAXLEN: usize = 32; +// This directly caps how much of each chunk the reranker can "see". +// Keep this in sync with chunk sizing; very large values quickly blow up MaxSim cost. +const DOC_MAXLEN: usize = 96; + +pub struct ColbertEncoderOrt { + session: Session, + tokenizer: Tokenizer, + hidden_size: usize, + // Special token IDs + cls_id: u32, + sep_id: u32, + mask_id: u32, + pad_id: u32, + query_marker_id: Option, + doc_marker_id: Option, + // Skip list for MaxSim (punctuation, special tokens to ignore) + skip_ids: HashSet, +} + +impl ColbertEncoderOrt { + pub fn load_from_hf(repo_id: &str, hidden_size: usize) -> anyhow::Result { + log_native(format!("[ColBERT-ORT] Downloading model from HF hub: {}", repo_id)); + + let api = Api::new()?; + let repo = api.repo(Repo::new(repo_id.to_string(), RepoType::Model)); + + // Download model and tokenizer files + // Try int8 quantized model first for speed, fall back to fp32 + let model_path = repo.get("onnx/model_int8.onnx") + .or_else(|_| repo.get("onnx/model.onnx"))?; + let tokenizer_path = repo.get("tokenizer.json")?; + + // Try to load skiplist + let skip_ids = match repo.get("skiplist.json") { + Ok(skiplist_path) => { + let content = std::fs::read_to_string(&skiplist_path)?; + let ids: Vec = serde_json::from_str(&content)?; + log_native(format!("[ColBERT-ORT] Loaded skiplist with {} token IDs", ids.len())); + ids.into_iter().collect() + } + Err(_) => { + log_native("[ColBERT-ORT] No skiplist found, using empty"); + HashSet::new() + } + }; + + log_native(format!("[ColBERT-ORT] Loading model from {:?}", model_path)); + + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? // Reduced from 8 to avoid thread contention + .commit_from_file(&model_path)?; + + let tokenizer = Tokenizer::from_file(&tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + // Get special token IDs + let vocab = tokenizer.get_vocab(true); + + let cls_id = *vocab.get("[CLS]").unwrap_or(&0); + let sep_id = *vocab.get("[SEP]").unwrap_or(&0); + let mask_id = *vocab.get("[MASK]").unwrap_or(&0); + let pad_id = *vocab.get("[PAD]").unwrap_or(&mask_id); // Use MASK as PAD if no PAD + + // ColBERT marker tokens (may not exist in all tokenizers) + let query_marker_id = vocab.get(QUERY_MARKER).copied(); + let doc_marker_id = vocab.get(DOC_MARKER).copied(); + + log_native(format!("[ColBERT-ORT] Token IDs: CLS={}, SEP={}, MASK={}, PAD={}", + cls_id, sep_id, mask_id, pad_id)); + log_native(format!("[ColBERT-ORT] Marker IDs: [Q]={:?}, [D]={:?}", + query_marker_id, doc_marker_id)); + log_native("[ColBERT-ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + cls_id, + sep_id, + mask_id, + pad_id, + query_marker_id, + doc_marker_id, + skip_ids, + }) + } + + /// Encode a query with ColBERT format: [CLS] [Q] tokens... [SEP] [MASK]... + /// Pads with [MASK] tokens to QUERY_MAXLEN for query expansion + pub fn encode_query(&mut self, text: &str) -> anyhow::Result { + // If the tokenizer doesn't have a dedicated [Q] token, mimic the Python + // harness behavior by prefixing the literal string "[Q] ". + let text_for_tokenizer; + let text = if self.query_marker_id.is_none() && !text.starts_with("[Q]") { + text_for_tokenizer = format!("[Q] {}", text); + text_for_tokenizer.as_str() + } else { + text + }; + + // Tokenize without special tokens + let encoding = self.tokenizer + .encode(text, false) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let token_ids = encoding.get_ids(); + + // Build sequence: [CLS] [Q]? tokens... [SEP] [MASK]... + let mut final_ids: Vec = Vec::with_capacity(QUERY_MAXLEN); + final_ids.push(self.cls_id); + + if let Some(q_id) = self.query_marker_id { + final_ids.push(q_id); + } + + // Add tokens (truncate if needed, leaving room for SEP) + let max_tokens = QUERY_MAXLEN - final_ids.len() - 1; // -1 for SEP + for &id in token_ids.iter().take(max_tokens) { + final_ids.push(id); + } + + final_ids.push(self.sep_id); + + // Pad with [MASK] for query expansion + while final_ids.len() < QUERY_MAXLEN { + final_ids.push(self.mask_id); + } + + // Create attention mask (all 1s, MASK tokens are attended) + let attention_mask: Vec = vec![1i64; final_ids.len()]; + let input_ids: Vec = final_ids.iter().map(|&id| id as i64).collect(); + + let seq_len = input_ids.len(); + + // Create tensors + let input_ids_tensor = Value::from_array(([1usize, seq_len], input_ids))?; + let attention_mask_tensor = Value::from_array(([1usize, seq_len], attention_mask))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids_tensor, + "attention_mask" => attention_mask_tensor + ])?; + + // Get embeddings [1, seq_len, hidden_size] + let embeddings_tensor = outputs[0].try_extract_tensor::()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Copy to owned vec and L2 normalize each token + let mut embeddings = vec![0.0f32; seq_len * self.hidden_size]; + for s in 0..seq_len { + let src_offset = s * self.hidden_size; + let dst_offset = s * self.hidden_size; + + // L2 normalize + let mut sum_sq = 0.0f32; + for d in 0..self.hidden_size { + let val = embeddings_data[src_offset + d]; + sum_sq += val * val; + } + let norm = sum_sq.sqrt().max(1e-12); + + for d in 0..self.hidden_size { + embeddings[dst_offset + d] = embeddings_data[src_offset + d] / norm; + } + } + + Ok(QueryEmbedding { + embeddings, + seq_len, + hidden_size: self.hidden_size, + }) + } + + /// Encode documents in a batch: [CLS] [D]? tokens... [SEP] + pub fn encode_docs(&mut self, texts: &[String]) -> anyhow::Result> { + if texts.is_empty() { + return Ok(vec![]); + } + + let batch_size = texts.len(); + + // Tokenize all texts + let mut all_token_ids: Vec> = Vec::with_capacity(batch_size); + let mut max_len = 0usize; + + for text in texts { + // If the tokenizer doesn't have a dedicated [D] token, mimic the Python + // harness behavior by prefixing the literal string "[D] ". + let text_for_tokenizer; + let text = if self.doc_marker_id.is_none() && !text.starts_with("[D]") { + text_for_tokenizer = format!("[D] {}", text); + text_for_tokenizer.as_str() + } else { + text.as_str() + }; + + let encoding = self.tokenizer + .encode(text, false) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let token_ids = encoding.get_ids(); + + // Build sequence: [CLS] [D]? tokens... [SEP] + let mut final_ids: Vec = Vec::with_capacity(DOC_MAXLEN); + final_ids.push(self.cls_id); + + if let Some(d_id) = self.doc_marker_id { + final_ids.push(d_id); + } + + // Add tokens (truncate if needed) + let max_tokens = DOC_MAXLEN - final_ids.len() - 1; + for &id in token_ids.iter().take(max_tokens) { + final_ids.push(id); + } + + final_ids.push(self.sep_id); + + if final_ids.len() > max_len { + max_len = final_ids.len(); + } + + all_token_ids.push(final_ids); + } + + // Pad to max_len and create batched tensors + let mut input_ids_vec = vec![0i64; batch_size * max_len]; + let mut attention_mask_vec = vec![0i64; batch_size * max_len]; + let mut real_lengths: Vec = Vec::with_capacity(batch_size); + + for (i, ids) in all_token_ids.iter().enumerate() { + let real_len = ids.len(); + real_lengths.push(real_len); + + for (j, &id) in ids.iter().enumerate() { + input_ids_vec[i * max_len + j] = id as i64; + attention_mask_vec[i * max_len + j] = 1; + } + // Remaining positions stay 0 (padded) + } + + // Create tensors + let input_ids = Value::from_array(([batch_size, max_len], input_ids_vec))?; + let attention_mask = Value::from_array(([batch_size, max_len], attention_mask_vec))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids, + "attention_mask" => attention_mask + ])?; + + // Get embeddings [batch, max_len, hidden_size] + let embeddings_tensor = outputs[0].try_extract_tensor::()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Extract per-document embeddings with L2 normalization + let mut results: Vec = Vec::with_capacity(batch_size); + + for b in 0..batch_size { + let real_len = real_lengths[b]; + let token_ids = &all_token_ids[b]; + let mut embeddings = vec![0.0f32; real_len * self.hidden_size]; + + for s in 0..real_len { + let src_offset = b * max_len * self.hidden_size + s * self.hidden_size; + let dst_offset = s * self.hidden_size; + + // L2 normalize each token embedding + let mut sum_sq = 0.0f32; + for d in 0..self.hidden_size { + let val = embeddings_data[src_offset + d]; + sum_sq += val * val; + } + let norm = sum_sq.sqrt().max(1e-12); + + for d in 0..self.hidden_size { + embeddings[dst_offset + d] = embeddings_data[src_offset + d] / norm; + } + } + + results.push(DocEmbedding { + embeddings, + token_ids: token_ids.clone(), + seq_len: real_len, + hidden_size: self.hidden_size, + }); + } + + Ok(results) + } + + /// MaxSim scoring: for each query token, find max similarity with doc tokens, sum + pub fn max_sim(&self, query: &QueryEmbedding, doc: &DocEmbedding) -> f32 { + let mut total_score = 0.0f32; + + for q in 0..query.seq_len { + let q_offset = q * query.hidden_size; + let mut max_dot = f32::NEG_INFINITY; + + for d in 0..doc.seq_len { + // Skip tokens in skiplist (punctuation, special tokens) + if self.skip_ids.contains(&doc.token_ids[d]) { + continue; + } + + let d_offset = d * doc.hidden_size; + + // Dot product (vectors are already L2 normalized) + let mut dot = 0.0f32; + for k in 0..query.hidden_size { + dot += query.embeddings[q_offset + k] * doc.embeddings[d_offset + k]; + } + + if dot > max_dot { + max_dot = dot; + } + } + + if max_dot > f32::NEG_INFINITY { + total_score += max_dot; + } + } + + total_score + } + + /// Rerank documents against a query, return sorted indices and scores + pub fn rerank(&mut self, query: &str, docs: &[String], top_k: usize) -> anyhow::Result { + use std::time::Instant; + + // Encode query + let t0 = Instant::now(); + let query_emb = self.encode_query(query)?; + let query_time = t0.elapsed(); + + // Encode docs in batches (larger batch = better throughput) + let t1 = Instant::now(); + let batch_size = 64; + let mut all_doc_embs: Vec = Vec::with_capacity(docs.len()); + + for chunk in docs.chunks(batch_size) { + let chunk_vec: Vec = chunk.to_vec(); + let embs = self.encode_docs(&chunk_vec)?; + all_doc_embs.extend(embs); + } + let doc_time = t1.elapsed(); + + // Score all docs + let t2 = Instant::now(); + let mut scores: Vec<(usize, f32)> = all_doc_embs + .iter() + .enumerate() + .map(|(i, doc_emb)| (i, self.max_sim(&query_emb, doc_emb))) + .collect(); + let score_time = t2.elapsed(); + + // Log timing once + static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!( + "[ColBERT-ORT] Timing: query={:?} docs={:?} maxsim={:?}", + query_time, doc_time, score_time + )); + } + + // Sort by score descending + scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top_k + let k = std::cmp::min(top_k, scores.len()); + let top_indices: Vec = scores[..k].iter().map(|(i, _)| *i as u32).collect(); + let top_scores: Vec = scores[..k].iter().map(|(_, s)| *s as f64).collect(); + let checksum: f64 = scores.iter().map(|(_, s)| *s as f64).sum(); + + Ok(RerankResultOrt { + indices: top_indices, + scores: top_scores, + checksum, + }) + } +} + +#[derive(Clone)] +pub struct QueryEmbedding { + pub embeddings: Vec, // [seq_len * hidden_size] flattened + pub seq_len: usize, + pub hidden_size: usize, +} + +#[derive(Clone)] +pub struct DocEmbedding { + pub embeddings: Vec, // [seq_len * hidden_size] flattened + pub token_ids: Vec, // For skiplist filtering + pub seq_len: usize, + pub hidden_size: usize, +} + +pub struct RerankResultOrt { + pub indices: Vec, + pub scores: Vec, + pub checksum: f64, +} + +/// Packed document embeddings for storage/retrieval +/// All embeddings are flattened into a single buffer with offsets +#[derive(Clone)] +pub struct PackedDocEmbeddings { + /// Flattened embeddings: all docs concatenated [sum(lengths) * hidden_size] + pub embeddings: Vec, + /// Token IDs for skiplist: all docs concatenated [sum(lengths)] + pub token_ids: Vec, + /// Number of tokens per document + pub lengths: Vec, + /// Byte offsets into embeddings buffer for each doc (for fast lookup) + pub offsets: Vec, + /// Hidden dimension + pub hidden_size: usize, +} + +impl ColbertEncoderOrt { + /// Encode documents and return packed embeddings for storage + /// This is for INDEX TIME - encode once, store, reuse at query time + pub fn encode_docs_packed(&mut self, texts: &[String]) -> anyhow::Result { + if texts.is_empty() { + return Ok(PackedDocEmbeddings { + embeddings: vec![], + token_ids: vec![], + lengths: vec![], + offsets: vec![], + hidden_size: self.hidden_size, + }); + } + + // Encode in batches + let batch_size = 64; + let mut all_embeddings: Vec = Vec::new(); + let mut all_token_ids: Vec = Vec::new(); + let mut lengths: Vec = Vec::with_capacity(texts.len()); + let mut offsets: Vec = Vec::with_capacity(texts.len()); + + for chunk in texts.chunks(batch_size) { + let chunk_vec: Vec = chunk.to_vec(); + let doc_embs = self.encode_docs(&chunk_vec)?; + + for doc in doc_embs { + offsets.push(all_embeddings.len() as u32); + lengths.push(doc.seq_len as u32); + all_embeddings.extend(doc.embeddings); + all_token_ids.extend(doc.token_ids); + } + } + + Ok(PackedDocEmbeddings { + embeddings: all_embeddings, + token_ids: all_token_ids, + lengths, + offsets, + hidden_size: self.hidden_size, + }) + } + + /// Score a query against pre-computed packed embeddings + /// This is for QUERY TIME - no doc encoding needed + pub fn score_packed( + &self, + query_emb: &QueryEmbedding, + packed: &PackedDocEmbeddings, + doc_indices: &[usize], // Which docs from packed to score + ) -> Vec { + let mut scores = Vec::with_capacity(doc_indices.len()); + + for &doc_idx in doc_indices { + if doc_idx >= packed.lengths.len() { + scores.push(0.0); + continue; + } + + let doc_len = packed.lengths[doc_idx] as usize; + let emb_offset = packed.offsets[doc_idx] as usize; + let token_offset: usize = packed.offsets[..doc_idx] + .iter() + .zip(&packed.lengths[..doc_idx]) + .map(|(&off, &len)| len as usize) + .sum(); + + // MaxSim scoring + let mut total_score = 0.0f32; + + for q in 0..query_emb.seq_len { + let q_offset = q * query_emb.hidden_size; + let mut max_dot = f32::NEG_INFINITY; + + for d in 0..doc_len { + // Check skiplist + let token_id = packed.token_ids[token_offset + d]; + if self.skip_ids.contains(&token_id) { + continue; + } + + let d_offset = emb_offset + d * packed.hidden_size; + + // Dot product + let mut dot = 0.0f32; + for k in 0..query_emb.hidden_size { + dot += query_emb.embeddings[q_offset + k] + * packed.embeddings[d_offset + k]; + } + + if dot > max_dot { + max_dot = dot; + } + } + + if max_dot > f32::NEG_INFINITY { + total_score += max_dot; + } + } + + scores.push(total_score); + } + + scores + } + + /// Rerank using pre-computed packed embeddings (FAST query-time path) + pub fn rerank_from_packed( + &mut self, + query: &str, + packed: &PackedDocEmbeddings, + doc_indices: &[usize], + top_k: usize, + ) -> anyhow::Result { + use std::time::Instant; + + // Encode query only + let t0 = Instant::now(); + let query_emb = self.encode_query(query)?; + let query_time = t0.elapsed(); + + // Score against packed embeddings + let t1 = Instant::now(); + let raw_scores = self.score_packed(&query_emb, packed, doc_indices); + let score_time = t1.elapsed(); + + // Log timing + static LOGGED_PACKED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED_PACKED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!( + "[ColBERT-ORT] Packed timing: query={:?} maxsim={:?}", + query_time, score_time + )); + } + + // Sort by score descending + let mut indexed_scores: Vec<(usize, f32)> = raw_scores + .iter() + .enumerate() + .map(|(i, &s)| (doc_indices[i], s)) + .collect(); + indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top_k + let k = std::cmp::min(top_k, indexed_scores.len()); + let top_indices: Vec = indexed_scores[..k].iter().map(|(i, _)| *i as u32).collect(); + let top_scores: Vec = indexed_scores[..k].iter().map(|(_, s)| *s as f64).collect(); + let checksum: f64 = raw_scores.iter().map(|&s| s as f64).sum(); + + Ok(RerankResultOrt { + indices: top_indices, + scores: top_scores, + checksum, + }) + } +} diff --git a/osgrep-core/src/dense_ort.rs b/osgrep-core/src/dense_ort.rs new file mode 100644 index 00000000..85b89467 --- /dev/null +++ b/osgrep-core/src/dense_ort.rs @@ -0,0 +1,208 @@ +use ort::session::{Session, builder::GraphOptimizationLevel}; +use ort::value::Value; +use tokenizers::Tokenizer; +use hf_hub::{api::sync::Api, Repo, RepoType}; + +fn log_native(msg: impl AsRef) { + // Intentionally no-op: native logging was polluting CLI output. + // If you need debugging, add structured logging at the JS layer instead. + let _ = msg.as_ref(); +} + +pub struct DenseEncoderOrt { + session: Session, + tokenizer: Tokenizer, + hidden_size: usize, +} + +impl DenseEncoderOrt { + /// Load ONNX model and tokenizer from HuggingFace Hub + /// repo_id: HF repo like "onnx-community/granite-embedding-30m-english-ONNX" + pub fn load_from_hf(repo_id: &str, hidden_size: usize) -> anyhow::Result { + log_native(format!("[ORT] Downloading model from HF hub: {}", repo_id)); + + let api = Api::new()?; + let repo = api.repo(Repo::new(repo_id.to_string(), RepoType::Model)); + + // Download model and tokenizer files + // ONNX model is in onnx/ subdirectory for onnx-community repos + // Also download the external data file if it exists + let model_path = repo.get("onnx/model.onnx")?; + let _ = repo.get("onnx/model.onnx_data"); // External data, ignore if not found + let tokenizer_path = repo.get("tokenizer.json")?; + + log_native(format!("[ORT] Loading model from {:?}", model_path)); + + // Initialize ONNX Runtime session with CPU provider + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? + .commit_from_file(&model_path)?; + + // Load tokenizer + let mut tokenizer = Tokenizer::from_file(&tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + // Configure truncation/padding (same as Candle) + let max_len = 256usize; + tokenizer.with_truncation(Some(tokenizers::TruncationParams { + max_length: max_len, + ..Default::default() + })).map_err(|e| anyhow::anyhow!("Failed to set truncation: {}", e))?; + + tokenizer.with_padding(Some(tokenizers::PaddingParams { + strategy: tokenizers::PaddingStrategy::BatchLongest, + ..Default::default() + })); + + log_native(format!("[ORT] Tokenizer configured with max_seq_len={}", max_len)); + log_native("[ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + }) + } + + /// Load ONNX model and tokenizer from local paths + pub fn load(model_path: &str, tokenizer_path: &str, hidden_size: usize) -> anyhow::Result { + log_native(format!("[ORT] Loading model from {}", model_path)); + + // Initialize ONNX Runtime session with CPU provider + let session = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(4)? + .commit_from_file(model_path)?; + + // Load tokenizer + let mut tokenizer = Tokenizer::from_file(tokenizer_path) + .map_err(|e| anyhow::anyhow!("Failed to load tokenizer: {}", e))?; + + let max_len = 256usize; + tokenizer.with_truncation(Some(tokenizers::TruncationParams { + max_length: max_len, + ..Default::default() + })).map_err(|e| anyhow::anyhow!("Failed to set truncation: {}", e))?; + + tokenizer.with_padding(Some(tokenizers::PaddingParams { + strategy: tokenizers::PaddingStrategy::BatchLongest, + ..Default::default() + })); + + log_native(format!("[ORT] Tokenizer configured with max_seq_len={}", max_len)); + log_native("[ORT] Model loaded successfully"); + + Ok(Self { + session, + tokenizer, + hidden_size, + }) + } + + pub fn encode_batch(&mut self, texts: Vec, normalize: bool) -> anyhow::Result> { + if texts.is_empty() { + return Err(anyhow::anyhow!("Empty input texts")); + } + + // Tokenize + let encodings = self.tokenizer + .encode_batch(texts.clone(), true) + .map_err(|e| anyhow::anyhow!("Tokenization failed: {}", e))?; + + let max_len = encodings.iter().map(|e| e.get_ids().len()).max().unwrap_or(0); + let batch_size = encodings.len(); + + // Log once + static LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + if !LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) { + log_native(format!("[ORT] First batch: batch_size={} max_seq_len={}", batch_size, max_len)); + } + + // Prepare input tensors + let mut input_ids_vec = vec![0i64; batch_size * max_len]; + let mut attention_mask_vec = vec![0i64; batch_size * max_len]; + let mut token_type_ids_vec = vec![0i64; batch_size * max_len]; + + for (i, encoding) in encodings.iter().enumerate() { + let ids = encoding.get_ids(); + let mask = encoding.get_attention_mask(); + let type_ids = encoding.get_type_ids(); + + for (j, &id) in ids.iter().enumerate() { + input_ids_vec[i * max_len + j] = id as i64; + } + for (j, &m) in mask.iter().enumerate() { + attention_mask_vec[i * max_len + j] = m as i64; + } + for (j, &t) in type_ids.iter().enumerate() { + token_type_ids_vec[i * max_len + j] = t as i64; + } + } + + // Create ORT tensors - pass Vec directly, not slice + let input_ids = Value::from_array(([batch_size, max_len], input_ids_vec))?; + let attention_mask_tensor = Value::from_array(([batch_size, max_len], attention_mask_vec.clone()))?; + let token_type_ids = Value::from_array(([batch_size, max_len], token_type_ids_vec))?; + + // Run inference + let outputs = self.session.run(ort::inputs![ + "input_ids" => input_ids, + "attention_mask" => attention_mask_tensor, + "token_type_ids" => token_type_ids + ])?; + + // Get the last_hidden_state output (typically first output) + let embeddings_tensor = outputs[0].try_extract_tensor::()?; + let embeddings_data: &[f32] = embeddings_tensor.1; + + // Mean pooling + let mut pooled = vec![0.0f32; batch_size * self.hidden_size]; + for i in 0..batch_size { + let mut sum_hidden = vec![0.0f32; self.hidden_size]; + let mut sum_mask = 0.0f32; + + for j in 0..max_len { + let mask_val = attention_mask_vec[i * max_len + j] as f32; + sum_mask += mask_val; + + for k in 0..self.hidden_size { + let emb_val = embeddings_data[i * max_len * self.hidden_size + j * self.hidden_size + k]; + sum_hidden[k] += emb_val * mask_val; + } + } + + let denom = sum_mask.max(1e-9); + for k in 0..self.hidden_size { + pooled[i * self.hidden_size + k] = sum_hidden[k] / denom; + } + } + + // L2 normalize if requested + if normalize { + for i in 0..batch_size { + let start = i * self.hidden_size; + let end = start + self.hidden_size; + let slice = &mut pooled[start..end]; + + let norm = slice.iter().map(|x| x * x).sum::().sqrt().max(1e-12); + for val in slice.iter_mut() { + *val /= norm; + } + } + } + + Ok(pooled) + } + + pub fn compute_checksum(&mut self, texts: Vec, normalize: bool) -> anyhow::Result { + let embeddings = self.encode_batch(texts, normalize)?; + let checksum: f64 = embeddings.iter().map(|&x| x as f64).sum(); + Ok(checksum) + } + + /// Get the hidden size (embedding dimension) + pub fn hidden_size(&self) -> usize { + self.hidden_size + } +} diff --git a/osgrep-core/src/lib.rs b/osgrep-core/src/lib.rs new file mode 100644 index 00000000..db8fae8a --- /dev/null +++ b/osgrep-core/src/lib.rs @@ -0,0 +1,265 @@ +//! osgrep-core: Fast embedding and reranking via ONNX Runtime +//! +//! This is the native performance core of osgrep. It provides: +//! - Dense embeddings (384-dim, granite-30m) +//! - ColBERT token embeddings (48-dim, mxbai-edge-colbert-17m) +//! - MaxSim reranking with pre-indexed documents + +#[macro_use] +extern crate napi_derive; + +use napi::bindgen_prelude::*; +use once_cell::sync::OnceCell; +use std::sync::Mutex; + +mod dense_ort; +mod colbert_ort; + +use dense_ort::DenseEncoderOrt; +use colbert_ort::{ColbertEncoderOrt, PackedDocEmbeddings}; + +// ============================================================================= +// Global Model Storage (initialized once, reused) +// ============================================================================= + +static DENSE_MODEL: OnceCell> = OnceCell::new(); +static COLBERT_MODEL: OnceCell> = OnceCell::new(); + +// ============================================================================= +// Initialization +// ============================================================================= + +/// Initialize both models. Call once at startup. +/// +/// dense_repo: HF repo like "onnx-community/granite-embedding-30m-english-ONNX" +/// colbert_repo: HF repo like "ryandono/mxbai-edge-colbert-v0-17m-onnx-int8" +#[napi] +pub fn init_models(dense_repo: String, colbert_repo: String) -> Result<()> { + // Initialize dense model + if DENSE_MODEL.get().is_none() { + let encoder = DenseEncoderOrt::load_from_hf(&dense_repo, 384) + .map_err(|e| Error::from_reason(format!("Failed to load dense model: {:?}", e)))?; + DENSE_MODEL.set(Mutex::new(encoder)) + .map_err(|_| Error::from_reason("Dense model already initialized"))?; + } + + // Initialize ColBERT model + if COLBERT_MODEL.get().is_none() { + let encoder = ColbertEncoderOrt::load_from_hf(&colbert_repo, 48) + .map_err(|e| Error::from_reason(format!("Failed to load ColBERT model: {:?}", e)))?; + COLBERT_MODEL.set(Mutex::new(encoder)) + .map_err(|_| Error::from_reason("ColBERT model already initialized"))?; + } + + Ok(()) +} + +/// Check if models are initialized +#[napi] +pub fn is_initialized() -> bool { + DENSE_MODEL.get().is_some() && COLBERT_MODEL.get().is_some() +} + +// ============================================================================= +// Dense Embeddings +// ============================================================================= + +#[napi(object)] +pub struct DenseResult { + /// Flat array of embeddings [batch_size * 384] + pub embeddings: Vec, + /// Number of texts encoded + pub count: u32, +} + +/// Encode texts to dense vectors (384-dim, L2-normalized) +#[napi] +pub fn embed_dense(texts: Vec) -> Result { + let model = DENSE_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock dense model: {:?}", e)))?; + + let embeddings_f32 = encoder.encode_batch(texts.clone(), true) + .map_err(|e| Error::from_reason(format!("Dense encoding failed: {:?}", e)))?; + + Ok(DenseResult { + embeddings: embeddings_f32.iter().map(|&x| x as f64).collect(), + count: texts.len() as u32, + }) +} + +// ============================================================================= +// ColBERT Embeddings (for indexing) +// ============================================================================= + +#[napi(object)] +pub struct ColbertPackedResult { + /// Packed embeddings as flat i8 array (all docs concatenated) + pub embeddings: Vec, + /// Token IDs for skiplist filtering + pub token_ids: Vec, + /// Number of tokens per document + pub lengths: Vec, + /// Byte offsets into embeddings for each doc + pub offsets: Vec, +} + +/// Encode documents to ColBERT embeddings (48-dim per token, packed for storage) +/// Call this at INDEX TIME to pre-compute embeddings +#[napi] +pub fn embed_colbert_packed(texts: Vec) -> Result { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let packed = encoder.encode_docs_packed(&texts) + .map_err(|e| Error::from_reason(format!("ColBERT encoding failed: {:?}", e)))?; + + // Convert f32 embeddings to i8 (quantized) + let embeddings_i8: Vec = packed.embeddings.iter() + .map(|&x| (x * 127.0).clamp(-128.0, 127.0) as i8) + .collect(); + + Ok(ColbertPackedResult { + embeddings: embeddings_i8, + token_ids: packed.token_ids, + lengths: packed.lengths, + offsets: packed.offsets, + }) +} + +// ============================================================================= +// ColBERT Reranking (for search) +// ============================================================================= + +#[napi(object)] +pub struct RerankResult { + /// Original indices of top-k documents + pub indices: Vec, + /// MaxSim scores for top-k documents + pub scores: Vec, +} + +/// Encode a query for ColBERT reranking +/// Returns the query matrix as flat f64 array [seq_len * 48] +#[napi] +pub fn encode_query_colbert(query: String) -> Result> { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let mut encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let query_emb = encoder.encode_query(&query) + .map_err(|e| Error::from_reason(format!("Query encoding failed: {:?}", e)))?; + + Ok(query_emb.embeddings.iter().map(|&x| x as f64).collect()) +} + +/// Rerank documents using pre-indexed ColBERT embeddings +/// +/// query_embeddings: flat f64 array from encode_query_colbert [seq_len * 48] +/// doc_embeddings: flat i8 array (packed ColBERT embeddings) +/// doc_lengths: number of tokens per document +/// doc_offsets: byte offset for each document in doc_embeddings +/// candidate_indices: which docs to rerank (e.g., top-100 from dense retrieval) +/// top_k: how many to return +#[napi] +pub fn rerank_colbert( + query_embeddings: Float64Array, + doc_embeddings: Int8Array, + doc_token_ids: Uint32Array, + doc_lengths: Vec, + doc_offsets: Vec, + candidate_indices: Vec, + top_k: u32, +) -> Result { + let model = COLBERT_MODEL.get() + .ok_or_else(|| Error::from_reason("Models not initialized. Call init_models() first."))?; + + let encoder = model.lock() + .map_err(|e| Error::from_reason(format!("Failed to lock ColBERT model: {:?}", e)))?; + + let query_embeddings = query_embeddings.to_vec(); + let doc_embeddings = doc_embeddings.to_vec(); + let doc_token_ids = doc_token_ids.to_vec(); + + let hidden_size = 48usize; + let query_seq_len = query_embeddings.len() / hidden_size; + + // Reconstruct query embedding struct + let query_emb = colbert_ort::QueryEmbedding { + embeddings: query_embeddings.iter().map(|&x| x as f32).collect(), + seq_len: query_seq_len, + hidden_size, + }; + + // Reconstruct packed doc embeddings (convert i8 back to f32) + let doc_embeddings_f32: Vec = doc_embeddings.iter() + .map(|&x| (x as f32) / 127.0) + .collect(); + + let packed = PackedDocEmbeddings { + embeddings: doc_embeddings_f32, + token_ids: doc_token_ids, + lengths: doc_lengths, + offsets: doc_offsets, + hidden_size, + }; + + // Score candidates + let indices: Vec = candidate_indices.iter().map(|&i| i as usize).collect(); + let scores = encoder.score_packed(&query_emb, &packed, &indices); + + // Sort by score descending + let mut indexed_scores: Vec<(usize, f32)> = indices.iter() + .zip(scores.iter()) + .map(|(&i, &s)| (i, s)) + .collect(); + indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); + + // Take top-k + let k = std::cmp::min(top_k as usize, indexed_scores.len()); + + Ok(RerankResult { + indices: indexed_scores[..k].iter().map(|(i, _)| *i as u32).collect(), + scores: indexed_scores[..k].iter().map(|(_, s)| *s as f64).collect(), + }) +} + +// ============================================================================= +// Convenience: Combined embed for indexing +// ============================================================================= + +#[napi(object)] +pub struct EmbedResult { + /// Dense embeddings [batch_size * 384] + pub dense: Vec, + /// Packed ColBERT embeddings (i8) + pub colbert_embeddings: Vec, + /// Token IDs for skiplist filtering (all docs concatenated) + pub colbert_token_ids: Vec, + /// Token counts per document + pub colbert_lengths: Vec, + /// Byte offsets per document + pub colbert_offsets: Vec, +} + +/// Embed texts for indexing (both dense and ColBERT in one call) +#[napi] +pub fn embed_batch(texts: Vec) -> Result { + let dense = embed_dense(texts.clone())?; + let colbert = embed_colbert_packed(texts)?; + + Ok(EmbedResult { + dense: dense.embeddings, + colbert_embeddings: colbert.embeddings, + colbert_token_ids: colbert.token_ids, + colbert_lengths: colbert.lengths, + colbert_offsets: colbert.offsets, + }) +} diff --git a/package.json b/package.json index fabd64ef..f95f0207 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,11 @@ "dev": "npx tsc && node dist/index.js", "test": "vitest run", "test:watch": "vitest", - "benchmark": "./run-benchmark.sh", - "benchmark:index": "./run-benchmark.sh $HOME/osgrep-benchmarks --index", - "benchmark:agent": "npx tsx src/bench/benchmark-agent.ts", - "benchmark:chart": "npx tsx src/bench/generate-benchmark-chart.ts", "format": "biome check --write .", "format:check": "biome check .", "lint": "biome lint .", "typecheck": "tsc --noEmit", - "prepublishOnly": "pnpm build" + "prepublishOnly": "bun run build" }, "keywords": [ "osgrep", @@ -40,15 +36,12 @@ "files": [ "dist", "plugins", - "README.md", - "LICENSE", - "NOTICE" + "README.md" ], "license": "Apache-2.0", "description": "Local grep-like search tool for your codebase.", "dependencies": { - "@clack/prompts": "^0.11.0", - "@huggingface/transformers": "^3.8.0", + "osgrep-core": "file:./osgrep-core", "@lancedb/lancedb": "^0.22.3", "@modelcontextprotocol/sdk": "^1.24.3", "apache-arrow": "^18.1.0", @@ -56,25 +49,15 @@ "chokidar": "^4.0.3", "cli-highlight": "^2.1.11", "commander": "^14.0.2", - "dotenv": "^17.2.3", "fast-glob": "^3.3.3", "ignore": "^5.0.0", "lmdb": "^3.4.4", - "onnxruntime-node": "1.21.0", "ora": "^5.4.1", - "piscina": "^5.1.4", - "simsimd": "^6.5.5", - "uuid": "^9.0.1", - "web-tree-sitter": "^0.25.10", - "zod": "^4.1.12" + "web-tree-sitter": "^0.25.10" }, "devDependencies": { - "@anthropic-ai/claude-agent-sdk": "^0.1.50", "@biomejs/biome": "2.3.4", "@types/node": "^24.10.1", - "@types/uuid": "^9.0.8", - "node-gyp": "^12.1.0", - "ts-node": "^10.9.2", "typescript": "^5.9.3", "vitest": "^1.6.0" } diff --git a/plugins/osgrep/hooks/start.js b/plugins/osgrep/hooks/start.js index 41066c87..9c8fa941 100644 --- a/plugins/osgrep/hooks/start.js +++ b/plugins/osgrep/hooks/start.js @@ -11,24 +11,72 @@ function readPayload() { } } +function isProcessRunning(pid) { + if (!Number.isFinite(pid) || pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function findProjectRoot(startDir) { + const start = _path.resolve(startDir || process.cwd()); + const osgrepDir = _path.join(start, ".osgrep"); + const gitDir = _path.join(start, ".git"); + if (fs.existsSync(osgrepDir) || fs.existsSync(gitDir)) return start; + return start; +} + +function getServerForProject(projectRoot) { + try { + const regPath = _path.join(require("node:os").homedir(), ".osgrep", "servers.json"); + if (!fs.existsSync(regPath)) return null; + const data = JSON.parse(fs.readFileSync(regPath, "utf-8")); + if (!Array.isArray(data)) return null; + return data.find((s) => s && s.projectRoot === projectRoot && isProcessRunning(s.pid)) || null; + } catch { + return null; + } +} + function main() { const payload = readPayload(); const cwd = payload.cwd || process.cwd(); + if (process.env.OSGREP_DISABLE_AUTO_SERVE === "1") { + const response = { + hookSpecificOutput: { + hookEventName: "SessionStart", + additionalContext: + "osgrep serve auto-start disabled (OSGREP_DISABLE_AUTO_SERVE=1).", + }, + }; + process.stdout.write(JSON.stringify(response)); + return; + } + + const projectRoot = findProjectRoot(cwd); + const existing = getServerForProject(projectRoot); const logPath = "/tmp/osgrep.log"; const out = fs.openSync(logPath, "a"); - const child = spawn("osgrep", ["serve"], { - cwd, - detached: true, - stdio: ["ignore", out, out], - }); - child.unref(); + if (!existing) { + const child = spawn("osgrep", ["serve", "--background"], { + cwd, + detached: true, + stdio: ["ignore", out, out], + }); + child.unref(); + } const response = { hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: - 'osgrep serve started; prefer `osgrep ""` over grep (plain output is agent-friendly).', + existing + ? `osgrep serve running (PID: ${existing.pid}, Port: ${existing.port}).` + : 'osgrep serve starting (indexing in background). Searches work immediately but may show partial results until indexing completes.', }, }; process.stdout.write(JSON.stringify(response)); diff --git a/plugins/osgrep/skills/osgrep/SKILL.md b/plugins/osgrep/skills/osgrep/SKILL.md index ab2549a1..34f71786 100644 --- a/plugins/osgrep/skills/osgrep/SKILL.md +++ b/plugins/osgrep/skills/osgrep/SKILL.md @@ -1,11 +1,12 @@ --- name: osgrep -description: Semantic code search. Use alongside grep - grep for exact strings, osgrep for concepts. +description: Semantic code search and call tracing. Use alongside grep - grep for exact strings, osgrep for concepts and call flow. allowed-tools: "Bash(osgrep:*), Read" --- ## What osgrep does + Finds code by meaning. When you'd ask a colleague "where do we handle auth?", use osgrep. - grep/ripgrep: exact string match, fast @@ -46,16 +47,45 @@ Read src/auth/handler.ts:45-120 Read the specific line range, not the whole file. -## Other commands +## Trace command + +When you need to understand call flow (who calls what, what calls who): + ```bash -# Trace call graph (who calls X, what X calls) -osgrep trace handleAuth +osgrep trace handleRequest +``` -# Skeleton of a huge file (to find which ranges to read) -osgrep skeleton src/giant-2000-line-file.ts +**Output:** +``` +handleRequest + def: src/server/handler.ts:45 + calls: validateAuth routeRequest sendResponse + called_by: src/index.ts:12 src/api/router.ts:87 +``` +Use trace when: +- You found a function and need to know what calls it +- You need to understand what a function depends on +- You're tracing request/data flow through the codebase + +```bash +# Only callers (who calls this?) +osgrep trace handleAuth --callers + +# Only callees (what does this call?) +osgrep trace handleAuth --callees + +# Filter to specific path +osgrep trace validateToken --path src/auth +``` + +## Other options +```bash # Just file paths when you only need locations osgrep "authentication" --compact + +# Show more results +osgrep "error handling" -m 20 ``` ## Workflow: architecture questions @@ -66,9 +96,6 @@ osgrep "where do requests enter the server" # 2. If you need deeper context on a specific function Read src/server/handler.ts:45-120 - -# 3. Trace to understand call flow -osgrep trace handleRequest ``` ## Tips diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 3f75cda9..00000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,4112 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@clack/prompts': - specifier: ^0.11.0 - version: 0.11.0 - '@huggingface/transformers': - specifier: ^3.8.0 - version: 3.8.1 - '@lancedb/lancedb': - specifier: ^0.22.3 - version: 0.22.3(apache-arrow@18.1.0) - '@modelcontextprotocol/sdk': - specifier: ^1.24.3 - version: 1.24.3(zod@4.1.13) - apache-arrow: - specifier: ^18.1.0 - version: 18.1.0 - chalk: - specifier: ^5.6.2 - version: 5.6.2 - chokidar: - specifier: ^4.0.3 - version: 4.0.3 - cli-highlight: - specifier: ^2.1.11 - version: 2.1.11 - commander: - specifier: ^14.0.2 - version: 14.0.2 - dotenv: - specifier: ^17.2.3 - version: 17.2.3 - fast-glob: - specifier: ^3.3.3 - version: 3.3.3 - ignore: - specifier: ^5.0.0 - version: 5.3.2 - lmdb: - specifier: ^3.4.4 - version: 3.4.4 - onnxruntime-node: - specifier: 1.21.0 - version: 1.21.0 - ora: - specifier: ^5.4.1 - version: 5.4.1 - piscina: - specifier: ^5.1.4 - version: 5.1.4 - simsimd: - specifier: ^6.5.5 - version: 6.5.5 - uuid: - specifier: ^9.0.1 - version: 9.0.1 - web-tree-sitter: - specifier: ^0.25.10 - version: 0.25.10 - zod: - specifier: ^4.1.12 - version: 4.1.13 - -devDependencies: - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.1.50 - version: 0.1.56(zod@4.1.13) - '@biomejs/biome': - specifier: 2.3.4 - version: 2.3.4 - '@types/node': - specifier: ^24.10.1 - version: 24.10.1 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 - node-gyp: - specifier: ^12.1.0 - version: 12.1.0 - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - vitest: - specifier: ^1.6.0 - version: 1.6.1(@types/node@24.10.1) - -packages: - - /@anthropic-ai/claude-agent-sdk@0.1.56(zod@4.1.13): - resolution: {integrity: sha512-r4IAJ3Wht2iTyS3qoDYbCEbkTNXM2W33DEt9q5tkDDD9UR0dU5U7qLPwA8TTTX4wyHkZic5a0jLDuD7xU3ZrZg==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.24.1 - dependencies: - zod: 4.1.13 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 - dev: true - - /@babel/runtime@7.28.4: - resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} - engines: {node: '>=6.9.0'} - requiresBuild: true - dev: false - optional: true - - /@biomejs/biome@2.3.4: - resolution: {integrity: sha512-TU08LXjBHdy0mEY9APtEtZdNQQijXUDSXR7IK1i45wgoPD5R0muK7s61QcFir6FpOj/RP1+YkPx5QJlycXUU3w==} - engines: {node: '>=14.21.3'} - hasBin: true - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.3.4 - '@biomejs/cli-darwin-x64': 2.3.4 - '@biomejs/cli-linux-arm64': 2.3.4 - '@biomejs/cli-linux-arm64-musl': 2.3.4 - '@biomejs/cli-linux-x64': 2.3.4 - '@biomejs/cli-linux-x64-musl': 2.3.4 - '@biomejs/cli-win32-arm64': 2.3.4 - '@biomejs/cli-win32-x64': 2.3.4 - dev: true - - /@biomejs/cli-darwin-arm64@2.3.4: - resolution: {integrity: sha512-w40GvlNzLaqmuWYiDU6Ys9FNhJiclngKqcGld3iJIiy2bpJ0Q+8n3haiaC81uTPY/NA0d8Q/I3Z9+ajc14102Q==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-darwin-x64@2.3.4: - resolution: {integrity: sha512-3s7TLVtjJ7ni1xADXsS7x7GMUrLBZXg8SemXc3T0XLslzvqKj/dq1xGeBQ+pOWQzng9MaozfacIHdK2UlJ3jGA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-arm64-musl@2.3.4: - resolution: {integrity: sha512-IruVGQRwMURivWazchiq7gKAqZSFs5so6gi0hJyxk7x6HR+iwZbO2IxNOqyLURBvL06qkIHs7Wffl6Bw30vCbQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-arm64@2.3.4: - resolution: {integrity: sha512-y7efHyyM2gYmHy/AdWEip+VgTMe9973aP7XYKPzu/j8JxnPHuSUXftzmPhkVw0lfm4ECGbdBdGD6+rLmTgNZaA==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-x64-musl@2.3.4: - resolution: {integrity: sha512-mzKFFv/w66e4/jCobFmD3kymCqG+FuWE7sVa4Yjqd9v7qt2UhXo67MSZKY9Ih18V2IwPzRKQPCw6KwdZs6AXSA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-linux-x64@2.3.4: - resolution: {integrity: sha512-gKfjWR/6/dfIxPJCw8REdEowiXCkIpl9jycpNVHux8aX2yhWPLjydOshkDL6Y/82PcQJHn95VCj7J+BRcE5o1Q==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-win32-arm64@2.3.4: - resolution: {integrity: sha512-5TJ6JfVez+yyupJ/iGUici2wzKf0RrSAxJhghQXtAEsc67OIpdwSKAQboemILrwKfHDi5s6mu7mX+VTCTUydkw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@biomejs/cli-win32-x64@2.3.4: - resolution: {integrity: sha512-FGCijXecmC4IedQ0esdYNlMpx0Jxgf4zceCaMu6fkjWyjgn50ZQtMiqZZQ0Q/77yqPxvtkgZAvt5uGw0gAAjig==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@clack/core@0.5.0: - resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: false - - /@clack/prompts@0.11.0: - resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} - dependencies: - '@clack/core': 0.5.0 - picocolors: 1.1.1 - sisteransi: 1.0.5 - dev: false - - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - - /@emnapi/runtime@1.7.1: - resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - requiresBuild: true - dependencies: - tslib: 2.8.1 - dev: false - optional: true - - /@esbuild/aix-ppc64@0.21.5: - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm64@0.21.5: - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.21.5: - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.21.5: - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.21.5: - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.21.5: - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.21.5: - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.21.5: - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.21.5: - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.21.5: - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.21.5: - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.21.5: - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.21.5: - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.21.5: - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.21.5: - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.21.5: - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.21.5: - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.21.5: - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.21.5: - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.21.5: - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.21.5: - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.21.5: - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.21.5: - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@huggingface/jinja@0.5.3: - resolution: {integrity: sha512-asqfZ4GQS0hD876Uw4qiUb7Tr/V5Q+JZuo2L+BtdrD4U40QU58nIRq3ZSgAzJgT874VLjhGVacaYfrdpXtEvtA==} - engines: {node: '>=18'} - dev: false - - /@huggingface/transformers@3.8.1: - resolution: {integrity: sha512-tsTk4zVjImqdqjS8/AOZg2yNLd1z9S5v+7oUPpXaasDRwEDhB+xnglK1k5cad26lL5/ZIaeREgWWy0bs9y9pPA==} - dependencies: - '@huggingface/jinja': 0.5.3 - onnxruntime-node: 1.21.0 - onnxruntime-web: 1.22.0-dev.20250409-89f8206ba4 - sharp: 0.34.5 - dev: false - - /@img/colour@1.0.0: - resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} - engines: {node: '>=18'} - dev: false - - /@img/sharp-darwin-arm64@0.33.5: - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-darwin-arm64@0.34.5: - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - dev: false - optional: true - - /@img/sharp-darwin-x64@0.33.5: - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-darwin-x64@0.34.5: - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - dev: false - optional: true - - /@img/sharp-libvips-darwin-arm64@1.0.4: - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-darwin-arm64@1.2.4: - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-darwin-x64@1.0.4: - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-darwin-x64@1.2.4: - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm64@1.0.4: - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-arm64@1.2.4: - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-arm@1.0.5: - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-arm@1.2.4: - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-ppc64@1.2.4: - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-riscv64@1.2.4: - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-s390x@1.2.4: - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linux-x64@1.0.4: - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linux-x64@1.2.4: - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-arm64@1.0.4: - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linuxmusl-arm64@1.2.4: - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-libvips-linuxmusl-x64@1.0.4: - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-libvips-linuxmusl-x64@1.2.4: - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-linux-arm64@0.33.5: - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linux-arm64@0.34.5: - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-arm@0.33.5: - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 - dev: true - optional: true - - /@img/sharp-linux-arm@0.34.5: - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-ppc64@0.34.5: - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-riscv64@0.34.5: - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-s390x@0.34.5: - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - dev: false - optional: true - - /@img/sharp-linux-x64@0.33.5: - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linux-x64@0.34.5: - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linuxmusl-arm64@0.33.5: - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linuxmusl-arm64@0.34.5: - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - dev: false - optional: true - - /@img/sharp-linuxmusl-x64@0.33.5: - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - dev: true - optional: true - - /@img/sharp-linuxmusl-x64@0.34.5: - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - requiresBuild: true - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - dev: false - optional: true - - /@img/sharp-wasm32@0.34.5: - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - requiresBuild: true - dependencies: - '@emnapi/runtime': 1.7.1 - dev: false - optional: true - - /@img/sharp-win32-arm64@0.34.5: - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-ia32@0.34.5: - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@img/sharp-win32-x64@0.33.5: - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@img/sharp-win32-x64@0.34.5: - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@isaacs/balanced-match@4.0.1: - resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} - engines: {node: 20 || >=22} - dev: true - - /@isaacs/brace-expansion@5.0.0: - resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/balanced-match': 4.0.1 - dev: true - - /@isaacs/fs-minipass@4.0.1: - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - dependencies: - minipass: 7.1.2 - - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@sinclair/typebox': 0.27.8 - dev: true - - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.5.5: - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - dev: true - - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - dev: true - - /@lancedb/lancedb-darwin-arm64@0.22.3: - resolution: {integrity: sha512-oP2Kic51nLqs27Xo+AzSVlcMgmmfZbU/PseQ3KBtU92rczO5DYU2St1Y7qDUWcjw+RF3H2v/DKzYed16h1wCBQ==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-darwin-x64@0.22.3: - resolution: {integrity: sha512-wOwgZkvBgQM8asjolz4NeyPa8W/AjZv4fwyQxJhTqKTGlB3ntrpdn1m84K5qncTmFFDcDfGgZ4DkNVkVK+ydoQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-arm64-gnu@0.22.3: - resolution: {integrity: sha512-YUbFuBKQniTZOR9h2/es1f7lDzdHNt8qXs5GaqFmLQv2GNWpnvKXVA/vVffhCNpFB5nV132o1VhXW3KoMubPsw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-arm64-musl@0.22.3: - resolution: {integrity: sha512-jVRMtXxxYaDlZSaclCIHB2N+NJvQ1Fj9EaPeBx+HxG2VqUg0vXKef+yiaD2aGo9sAH6mMmkKJsrPhwABpUC4rQ==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-x64-gnu@0.22.3: - resolution: {integrity: sha512-co7idTwvNAtbFoFHojhHlTpKsydOm5sZfbtAsQRdoa7g6a61yIrqrMm8D7Ngh756JfzZLFQBMkDUZEW3X4vP/g==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-linux-x64-musl@0.22.3: - resolution: {integrity: sha512-+ipFsn5PCODK7mOMq1gZ5OAZWks5YlgmjAlnYMmU8XxvaK0b6lZdA3s1hTmBaBO9+wv+31ulO55oBN4/U8Yldg==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-win32-arm64-msvc@0.22.3: - resolution: {integrity: sha512-E0XywJYnelIe4pzOlvog+aMHKt5ChW27tgmT2V80Z6PXcX6eN9I69Fj0Q6DK6z1YCTPIPu6Na1Hd6d4GqUNKPw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb-win32-x64-msvc@0.22.3: - resolution: {integrity: sha512-/1feFnjz5MIhzXOEU4+1OeGwpAFYczGfefuOGZRsmGWDdt4V6/fza7Hkkxyb2OnTzqpBfy6BdW2+iBguE1JMyQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lancedb/lancedb@0.22.3(apache-arrow@18.1.0): - resolution: {integrity: sha512-nRC0fkg+d7dzCtudKHT+VH7znk6KUXRZyuS6HJYNnIrbvXBxaT6wAPjEbf70KTuqvP2znj48Zg+kiwRqkRnAJw==} - engines: {node: '>= 18'} - os: [darwin, linux, win32] - peerDependencies: - apache-arrow: '>=15.0.0 <=18.1.0' - dependencies: - apache-arrow: 18.1.0 - reflect-metadata: 0.2.2 - optionalDependencies: - '@lancedb/lancedb-darwin-arm64': 0.22.3 - '@lancedb/lancedb-darwin-x64': 0.22.3 - '@lancedb/lancedb-linux-arm64-gnu': 0.22.3 - '@lancedb/lancedb-linux-arm64-musl': 0.22.3 - '@lancedb/lancedb-linux-x64-gnu': 0.22.3 - '@lancedb/lancedb-linux-x64-musl': 0.22.3 - '@lancedb/lancedb-win32-arm64-msvc': 0.22.3 - '@lancedb/lancedb-win32-x64-msvc': 0.22.3 - dev: false - - /@lmdb/lmdb-darwin-arm64@3.4.4: - resolution: {integrity: sha512-XaKL705gDWd6XVls3ATDj13ZdML/LqSIxwgnYpG8xTzH2ifArx8fMMDdvqGE/Emd+W6R90W2fveZcJ0AyS8Y0w==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-darwin-x64@3.4.4: - resolution: {integrity: sha512-GPHGEVcwJlkD01GmIr7B4kvbIcUDS2+kBadVEd7lU4can1RZaZQLDDBJRrrNfS2Kavvl0VLI/cMv7UASAXGrww==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-arm64@3.4.4: - resolution: {integrity: sha512-mALqr7DE42HsiwVTKpQWxacjHoJk+e9p00RWIJqTACh/hpucxp/0lK/XMh5XzWnU/TDCZLukq1+vNqnNumTP/Q==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-arm@3.4.4: - resolution: {integrity: sha512-cmev5/dZr5ACKri9f6GU6lZCXTjMhV72xujlbOhFCgFXrt4W0TxGsmY8kA1BITvH60JBKE50cSxsiulybAbrrw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-linux-x64@3.4.4: - resolution: {integrity: sha512-QjLs8OcmCNcraAcLoZyFlo0atzBJniQLLwhtR+ymQqS5kLYpV5RqwriL87BW+ZiR9ZiGgZx3evrz5vnWPtJ1fQ==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-win32-arm64@3.4.4: - resolution: {integrity: sha512-tr/pwHDlZ33forLGAr0tI04cRmP4SgF93yHbb+2zvZiDEyln5yMHhbKDySxY66aUOkhvBvTuHq9q/3YmTj6ZHQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@lmdb/lmdb-win32-x64@3.4.4: - resolution: {integrity: sha512-KRzfocJzB/mgoTCqnMawuLSKheHRVTqWfSmouIgYpFs6Hx4zvZSvsZKSCEb5gHmICy7qsx9l06jk3MFTtiFVAQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@modelcontextprotocol/sdk@1.24.3(zod@4.1.13): - resolution: {integrity: sha512-YgSHW29fuzKKAHTGe9zjNoo+yF8KaQPzDC2W9Pv41E7/57IfY+AMGJ/aDFlgTLcVVELoggKE4syABCE75u3NCw==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.1.13 - zod-to-json-schema: 3.25.0(zod@4.1.13) - transitivePeerDependencies: - - supports-color - dev: false - - /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3: - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3: - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3: - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3: - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3: - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3: - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-android-arm-eabi@1.1.1: - resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-android-arm64@1.1.1: - resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-darwin-arm64@1.1.1: - resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-darwin-x64@1.1.1: - resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-freebsd-x64@1.1.1: - resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm-gnueabihf@1.1.1: - resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm64-gnu@1.1.1: - resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-arm64-musl@1.1.1: - resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-ppc64-gnu@1.1.1: - resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} - engines: {node: '>= 10'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-riscv64-gnu@1.1.1: - resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-s390x-gnu@1.1.1: - resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} - engines: {node: '>= 10'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-x64-gnu@1.1.1: - resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-linux-x64-musl@1.1.1: - resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-openharmony-arm64@1.1.1: - resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [openharmony] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-arm64-msvc@1.1.1: - resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-ia32-msvc@1.1.1: - resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice-win32-x64-msvc@1.1.1: - resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@napi-rs/nice@1.1.1: - resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} - engines: {node: '>= 10'} - requiresBuild: true - optionalDependencies: - '@napi-rs/nice-android-arm-eabi': 1.1.1 - '@napi-rs/nice-android-arm64': 1.1.1 - '@napi-rs/nice-darwin-arm64': 1.1.1 - '@napi-rs/nice-darwin-x64': 1.1.1 - '@napi-rs/nice-freebsd-x64': 1.1.1 - '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 - '@napi-rs/nice-linux-arm64-gnu': 1.1.1 - '@napi-rs/nice-linux-arm64-musl': 1.1.1 - '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 - '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 - '@napi-rs/nice-linux-s390x-gnu': 1.1.1 - '@napi-rs/nice-linux-x64-gnu': 1.1.1 - '@napi-rs/nice-linux-x64-musl': 1.1.1 - '@napi-rs/nice-openharmony-arm64': 1.1.1 - '@napi-rs/nice-win32-arm64-msvc': 1.1.1 - '@napi-rs/nice-win32-ia32-msvc': 1.1.1 - '@napi-rs/nice-win32-x64-msvc': 1.1.1 - dev: false - optional: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: false - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: false - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - dev: false - - /@npmcli/agent@4.0.0: - resolution: {integrity: sha512-kAQTcEN9E8ERLVg5AsGwLNoFb+oEG6engbqAU2P43gD4JEIkNGMHdVQ096FsOAAYpZPB0RSt0zgInKIAS1l5QA==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - agent-base: 7.1.4 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 11.2.4 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - dev: true - - /@npmcli/fs@5.0.0: - resolution: {integrity: sha512-7OsC1gNORBEawOa5+j2pXN9vsicaIOH5cPXxoR6fJOmH6/EXpJB2CajXOu1fPRFun2m1lktEFX11+P89hqO/og==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - semver: 7.7.3 - dev: true - - /@protobufjs/aspromise@1.1.2: - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - dev: false - - /@protobufjs/base64@1.1.2: - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - dev: false - - /@protobufjs/codegen@2.0.4: - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - dev: false - - /@protobufjs/eventemitter@1.1.0: - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - dev: false - - /@protobufjs/fetch@1.1.0: - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - dev: false - - /@protobufjs/float@1.0.2: - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - dev: false - - /@protobufjs/inquire@1.1.0: - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - dev: false - - /@protobufjs/path@1.1.2: - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - dev: false - - /@protobufjs/pool@1.1.0: - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - dev: false - - /@protobufjs/utf8@1.1.0: - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - dev: false - - /@rollup/rollup-android-arm-eabi@4.53.3: - resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-android-arm64@4.53.3: - resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-arm64@4.53.3: - resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-darwin-x64@4.53.3: - resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-arm64@4.53.3: - resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-freebsd-x64@4.53.3: - resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-gnueabihf@4.53.3: - resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm-musleabihf@4.53.3: - resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-gnu@4.53.3: - resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-arm64-musl@4.53.3: - resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-loong64-gnu@4.53.3: - resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-ppc64-gnu@4.53.3: - resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-gnu@4.53.3: - resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-riscv64-musl@4.53.3: - resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-s390x-gnu@4.53.3: - resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-gnu@4.53.3: - resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-linux-x64-musl@4.53.3: - resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-openharmony-arm64@4.53.3: - resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} - cpu: [arm64] - os: [openharmony] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-arm64-msvc@4.53.3: - resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-ia32-msvc@4.53.3: - resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-gnu@4.53.3: - resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@rollup/rollup-win32-x64-msvc@4.53.3: - resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true - - /@swc/helpers@0.5.17: - resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} - dependencies: - tslib: 2.8.1 - dev: false - - /@tsconfig/node10@1.0.12: - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - dev: true - - /@tsconfig/node12@1.0.11: - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true - - /@tsconfig/node14@1.0.3: - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true - - /@tsconfig/node16@1.0.4: - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - dev: true - - /@types/command-line-args@5.2.3: - resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} - dev: false - - /@types/command-line-usage@5.0.4: - resolution: {integrity: sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==} - dev: false - - /@types/estree@1.0.8: - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - dev: true - - /@types/node@20.19.25: - resolution: {integrity: sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==} - dependencies: - undici-types: 6.21.0 - dev: false - - /@types/node@24.10.1: - resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - dependencies: - undici-types: 7.16.0 - - /@types/uuid@9.0.8: - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - dev: true - - /@vitest/expect@1.6.1: - resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} - dependencies: - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - chai: 4.5.0 - dev: true - - /@vitest/runner@1.6.1: - resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} - dependencies: - '@vitest/utils': 1.6.1 - p-limit: 5.0.0 - pathe: 1.1.2 - dev: true - - /@vitest/snapshot@1.6.1: - resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} - dependencies: - magic-string: 0.30.21 - pathe: 1.1.2 - pretty-format: 29.7.0 - dev: true - - /@vitest/spy@1.6.1: - resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} - dependencies: - tinyspy: 2.2.1 - dev: true - - /@vitest/utils@1.6.1: - resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 - dev: true - - /abbrev@4.0.0: - resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} - engines: {node: ^20.17.0 || >=22.9.0} - dev: true - - /accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - dev: false - - /acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - dependencies: - acorn: 8.15.0 - dev: true - - /acorn@8.15.0: - resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - dev: true - - /ajv-formats@3.0.1(ajv@8.17.1): - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - dependencies: - ajv: 8.17.1 - dev: false - - /ajv@8.17.1: - resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - dev: false - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: false - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: false - - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true - - /any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - dev: false - - /apache-arrow@18.1.0: - resolution: {integrity: sha512-v/ShMp57iBnBp4lDgV8Jx3d3Q5/Hac25FWmQ98eMahUiHPXcvwIMKJD0hBIgclm/FCG+LwPkAKtkRO1O/W0YGg==} - hasBin: true - dependencies: - '@swc/helpers': 0.5.17 - '@types/command-line-args': 5.2.3 - '@types/command-line-usage': 5.0.4 - '@types/node': 20.19.25 - command-line-args: 5.2.1 - command-line-usage: 7.0.3 - flatbuffers: 24.12.23 - json-bignum: 0.0.3 - tslib: 2.8.1 - dev: false - - /arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: true - - /array-back@3.1.0: - resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} - engines: {node: '>=6'} - dev: false - - /array-back@6.2.2: - resolution: {integrity: sha512-gUAZ7HPyb4SJczXAMUXMGAvI976JoK3qEx9v1FTmeYuJj0IBiaKttG1ydtGKdkfqWkIkouke7nG8ufGy77+Cvw==} - engines: {node: '>=12.17'} - dev: false - - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true - - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: false - - /benchmark@2.1.4: - resolution: {integrity: sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ==} - requiresBuild: true - dependencies: - lodash: 4.17.21 - platform: 1.3.6 - dev: false - optional: true - - /bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - dependencies: - file-uri-to-path: 1.0.0 - dev: false - - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - dev: false - - /body-parser@2.2.1: - resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} - engines: {node: '>=18'} - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.0 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - dev: false - - /boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - dev: false - - /braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.1.1 - dev: false - - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - dev: false - - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - dev: false - - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true - - /cacache@20.0.3: - resolution: {integrity: sha512-3pUp4e8hv07k1QlijZu6Kn7c9+ZpWWk4j3F8N3xPuCExULobqJydKYOTj1FTq58srkJsXvO7LbGAH4C0ZU3WGw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - '@npmcli/fs': 5.0.0 - fs-minipass: 3.0.3 - glob: 13.0.0 - lru-cache: 11.2.4 - minipass: 7.1.2 - minipass-collect: 2.0.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - p-map: 7.0.4 - ssri: 13.0.0 - unique-filename: 5.0.0 - dev: true - - /call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - dev: false - - /call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - dev: false - - /chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 - dev: true - - /chalk-template@0.4.0: - resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} - engines: {node: '>=12'} - dependencies: - chalk: 4.1.2 - dev: false - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: false - - /chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: false - - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - dependencies: - readdirp: 4.1.2 - dev: false - - /chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - dependencies: - restore-cursor: 3.1.0 - dev: false - - /cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - dev: false - - /cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - dev: false - - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: false - - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: false - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /command-line-args@5.2.1: - resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - find-replace: 3.0.0 - lodash.camelcase: 4.3.0 - typical: 4.0.0 - dev: false - - /command-line-usage@7.0.3: - resolution: {integrity: sha512-PqMLy5+YGwhMh1wS04mVG44oqDsgyLRSKJBdOo1bnYhMKBW65gZF1dRp2OZRhiTjgUHljy99qkO7bsctLaw35Q==} - engines: {node: '>=12.20.0'} - dependencies: - array-back: 6.2.2 - chalk-template: 0.4.0 - table-layout: 4.1.1 - typical: 7.3.0 - dev: false - - /commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} - dev: false - - /complex.js@2.4.3: - resolution: {integrity: sha512-UrQVSUur14tNX6tiP4y8T4w4FeJAX3bi2cIv0pu/DTLFNxoq7z2Yh83Vfzztj6Px3X/lubqQ9IrPp7Bpn6p4MQ==} - requiresBuild: true - dev: false - optional: true - - /confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - dev: true - - /content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - dev: false - - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - dev: false - - /cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - dev: false - - /cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - dev: false - - /cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - dev: false - - /create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true - - /cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - /debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.3 - - /decimal.js@10.6.0: - resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - requiresBuild: true - dev: false - optional: true - - /deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} - dependencies: - type-detect: 4.1.0 - dev: true - - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - dependencies: - clone: 1.0.4 - dev: false - - /define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: false - - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - dev: false - - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dev: false - - /detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - dev: false - - /detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - dev: false - - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true - - /diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dev: true - - /dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} - engines: {node: '>=12'} - dev: false - - /dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - dev: false - - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - dev: false - - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: false - - /encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - dev: false - - /encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true - dependencies: - iconv-lite: 0.6.3 - dev: true - optional: true - - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - dev: true - - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true - - /es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - dev: false - - /es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - dev: false - - /es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - dev: false - - /es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - dev: false - - /esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - 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 - dev: true - - /escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - dev: false - - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - dev: false - - /escape-latex@1.2.0: - resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==} - requiresBuild: true - dev: false - optional: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: false - - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - dependencies: - '@types/estree': 1.0.8 - dev: true - - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - dev: false - - /eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - dev: false - - /eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - dependencies: - eventsource-parser: 3.0.6 - dev: false - - /execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - dev: true - - /exponential-backoff@3.1.3: - resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - dev: true - - /express-rate-limit@7.5.1(express@5.2.1): - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - dependencies: - express: 5.2.1 - dev: false - - /express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - dependencies: - accepts: 2.0.0 - body-parser: 2.2.1 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: false - - /fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - dev: false - - /fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - dev: false - - /fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - dependencies: - reusify: 1.1.0 - dev: false - - /fdir@6.5.0(picomatch@4.0.3): - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - dependencies: - picomatch: 4.0.3 - dev: true - - /file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - dev: false - - /fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: false - - /finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /find-replace@3.0.0: - resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} - engines: {node: '>=4.0.0'} - dependencies: - array-back: 3.1.0 - dev: false - - /flatbuffers@24.12.23: - resolution: {integrity: sha512-dLVCAISd5mhls514keQzmEG6QHmUUsNuWsb4tFafIUwvvgDjXhtfAYSKOzt5SWOy+qByV5pbsDZ+Vb7HUOBEdA==} - dev: false - - /flatbuffers@25.9.23: - resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} - dev: false - - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - dev: false - - /fraction.js@5.3.4: - resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - requiresBuild: true - dev: false - optional: true - - /fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - dev: false - - /fs-minipass@3.0.3: - resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dependencies: - minipass: 7.1.2 - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: false - - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: false - - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true - - /get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - dev: false - - /get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - dev: false - - /get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: false - - /glob@13.0.0: - resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} - engines: {node: 20 || >=22} - dependencies: - minimatch: 10.1.1 - minipass: 7.1.2 - path-scurry: 2.0.1 - dev: true - - /global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - dependencies: - boolean: 3.2.0 - es6-error: 4.1.1 - matcher: 3.0.0 - roarr: 2.15.4 - semver: 7.7.3 - serialize-error: 7.0.1 - dev: false - - /globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - dev: false - - /gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - dev: false - - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: true - - /guid-typescript@1.0.9: - resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} - dev: false - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: false - - /has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - dependencies: - es-define-property: 1.0.1 - dev: false - - /has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - dev: false - - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: false - - /highlight.js@10.7.3: - resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - dev: false - - /http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - dev: true - - /http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - dev: false - - /http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - dev: true - - /https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - dev: true - - /human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - dev: true - - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - requiresBuild: true - dependencies: - safer-buffer: 2.1.2 - dev: true - optional: true - - /iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - dependencies: - safer-buffer: 2.1.2 - dev: false - - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false - - /ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - dev: false - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: false - - /ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - dev: true - - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - dev: false - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: false - - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - dev: false - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: false - - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: false - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: false - - /is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - dev: false - - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true - - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - /isexe@3.1.1: - resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} - engines: {node: '>=16'} - dev: true - - /javascript-natural-sort@0.7.1: - resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} - requiresBuild: true - dev: false - optional: true - - /jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - dev: false - - /js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - dev: true - - /json-bignum@0.0.3: - resolution: {integrity: sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==} - engines: {node: '>=0.8'} - dev: false - - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - dev: false - - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - dev: false - - /lmdb@3.4.4: - resolution: {integrity: sha512-+Y2DqovevLkb6DrSQ6SXTYLEd6kvlRbhsxzgJrk7BUfOVA/mt21ak6pFDZDKxiAczHMWxrb02kXBTSTIA0O94A==} - hasBin: true - requiresBuild: true - dependencies: - msgpackr: 1.11.5 - node-addon-api: 6.1.0 - node-gyp-build-optional-packages: 5.2.2 - ordered-binary: 1.6.0 - weak-lru-cache: 1.2.2 - optionalDependencies: - '@lmdb/lmdb-darwin-arm64': 3.4.4 - '@lmdb/lmdb-darwin-x64': 3.4.4 - '@lmdb/lmdb-linux-arm': 3.4.4 - '@lmdb/lmdb-linux-arm64': 3.4.4 - '@lmdb/lmdb-linux-x64': 3.4.4 - '@lmdb/lmdb-win32-arm64': 3.4.4 - '@lmdb/lmdb-win32-x64': 3.4.4 - dev: false - - /local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - dependencies: - mlly: 1.8.0 - pkg-types: 1.3.1 - dev: true - - /lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - requiresBuild: true - dev: false - optional: true - - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - dev: false - - /long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - dev: false - - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - dependencies: - get-func-name: 2.0.2 - dev: true - - /lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} - engines: {node: 20 || >=22} - dev: true - - /magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - dev: true - - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: true - - /make-fetch-happen@15.0.3: - resolution: {integrity: sha512-iyyEpDty1mwW3dGlYXAJqC/azFn5PPvgKVwXayOGBSmKLxhKZ9fg4qIan2ePpp1vJIwfFiO34LAPZgq9SZW9Aw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - '@npmcli/agent': 4.0.0 - cacache: 20.0.3 - http-cache-semantics: 4.2.0 - minipass: 7.1.2 - minipass-fetch: 5.0.0 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 1.0.0 - proc-log: 6.1.0 - promise-retry: 2.0.1 - ssri: 13.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - dependencies: - escape-string-regexp: 4.0.0 - dev: false - - /math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - dev: false - - /mathjs@14.9.1: - resolution: {integrity: sha512-xhqv8Xjf+caWG3WlaPekg4v8QFOR3D5+8ycfcjMcPcnCNDgAONQLaLfyGgrggJrcHx2yUGCpACRpiD4GmXwX+Q==} - engines: {node: '>= 18'} - hasBin: true - requiresBuild: true - dependencies: - '@babel/runtime': 7.28.4 - complex.js: 2.4.3 - decimal.js: 10.6.0 - escape-latex: 1.2.0 - fraction.js: 5.3.4 - javascript-natural-sort: 0.7.1 - seedrandom: 3.0.5 - tiny-emitter: 2.1.0 - typed-function: 4.2.2 - dev: false - optional: true - - /media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - dev: false - - /merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - dev: false - - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: false - - /micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - dev: false - - /mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - dev: false - - /mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - dependencies: - mime-db: 1.54.0 - dev: false - - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: false - - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true - - /minimatch@10.1.1: - resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} - engines: {node: 20 || >=22} - dependencies: - '@isaacs/brace-expansion': 5.0.0 - dev: true - - /minipass-collect@2.0.1: - resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - minipass: 7.1.2 - dev: true - - /minipass-fetch@5.0.0: - resolution: {integrity: sha512-fiCdUALipqgPWrOVTz9fw0XhcazULXOSU6ie40DDbX1F49p1dBrSRBuswndTx1x3vEb/g0FT7vC4c4C2u/mh3A==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - minipass: 7.1.2 - minipass-sized: 1.0.3 - minizlib: 3.1.0 - optionalDependencies: - encoding: 0.1.13 - dev: true - - /minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - dependencies: - minipass: 3.3.6 - dev: true - - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - dependencies: - yallist: 4.0.0 - dev: true - - /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - /minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - dependencies: - minipass: 7.1.2 - - /mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} - dependencies: - acorn: 8.15.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.1 - dev: true - - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - /msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - requiresBuild: true - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - dev: false - optional: true - - /msgpackr@1.11.5: - resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} - optionalDependencies: - msgpackr-extract: 3.0.3 - dev: false - - /mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - dev: false - - /nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - /node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - dev: false - - /node-addon-api@8.5.0: - resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==} - engines: {node: ^18 || ^20 || >= 21} - dev: false - - /node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - dependencies: - detect-libc: 2.1.2 - dev: false - - /node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - dev: false - - /node-gyp@12.1.0: - resolution: {integrity: sha512-W+RYA8jBnhSr2vrTtlPYPc1K+CSjGpVDRZxcqJcERZ8ND3A1ThWPHRwctTx3qC3oW99jt726jhdz3Y6ky87J4g==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - env-paths: 2.2.1 - exponential-backoff: 3.1.3 - graceful-fs: 4.2.11 - make-fetch-happen: 15.0.3 - nopt: 9.0.0 - proc-log: 6.1.0 - semver: 7.7.3 - tar: 7.5.2 - tinyglobby: 0.2.15 - which: 6.0.0 - transitivePeerDependencies: - - supports-color - dev: true - - /nopt@9.0.0: - resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - abbrev: 4.0.0 - dev: true - - /npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dependencies: - path-key: 4.0.0 - dev: true - - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false - - /object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - dev: false - - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - dev: false - - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - dependencies: - ee-first: 1.1.1 - dev: false - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: false - - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - dependencies: - mimic-fn: 2.1.0 - dev: false - - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - dependencies: - mimic-fn: 4.0.0 - dev: true - - /onnxruntime-common@1.21.0: - resolution: {integrity: sha512-Q632iLLrtCAVOTO65dh2+mNbQir/QNTVBG3h/QdZBpns7mZ0RYbLRBgGABPbpU9351AgYy7SJf1WaeVwMrBFPQ==} - dev: false - - /onnxruntime-common@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-vDJMkfCfb0b1A836rgHj+ORuZf4B4+cc2bASQtpeoJLueuFc5DuYwjIZUBrSvx/fO5IrLjLz+oTrB3pcGlhovQ==} - dev: false - - /onnxruntime-node@1.21.0: - resolution: {integrity: sha512-NeaCX6WW2L8cRCSqy3bInlo5ojjQqu2fD3D+9W5qb5irwxhEyWKXeH2vZ8W9r6VxaMPUan+4/7NDwZMtouZxEw==} - os: [win32, darwin, linux] - requiresBuild: true - dependencies: - global-agent: 3.0.0 - onnxruntime-common: 1.21.0 - tar: 7.5.2 - dev: false - - /onnxruntime-web@1.22.0-dev.20250409-89f8206ba4: - resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==} - dependencies: - flatbuffers: 25.9.23 - guid-typescript: 1.0.9 - long: 5.3.2 - onnxruntime-common: 1.22.0-dev.20250409-89f8206ba4 - platform: 1.3.6 - protobufjs: 7.5.4 - dev: false - - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - dev: false - - /ordered-binary@1.6.0: - resolution: {integrity: sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==} - dev: false - - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.2.2 - dev: true - - /p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} - engines: {node: '>=18'} - dev: true - - /parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - dependencies: - parse5: 6.0.1 - dev: false - - /parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - dev: false - - /parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - dev: false - - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - dev: false - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true - - /path-scurry@2.0.1: - resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} - engines: {node: 20 || >=22} - dependencies: - lru-cache: 11.2.4 - minipass: 7.1.2 - dev: true - - /path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - dev: false - - /pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - dev: true - - /pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - dev: true - - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true - - /picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: false - - /picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - dev: true - - /piscina@5.1.4: - resolution: {integrity: sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==} - engines: {node: '>=20.x'} - optionalDependencies: - '@napi-rs/nice': 1.1.1 - dev: false - - /pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - dev: false - - /pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - dependencies: - confbox: 0.1.8 - mlly: 1.8.0 - pathe: 2.0.3 - dev: true - - /platform@1.3.6: - resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} - dev: false - - /postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - dev: true - - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - dev: true - - /proc-log@6.1.0: - resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} - engines: {node: ^20.17.0 || >=22.9.0} - dev: true - - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - dev: true - - /protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - requiresBuild: true - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 24.10.1 - long: 5.3.2 - dev: false - - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - dev: false - - /qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - dependencies: - side-channel: 1.1.0 - dev: false - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: false - - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - dev: false - - /raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.0 - unpipe: 1.0.0 - dev: false - - /react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - dev: true - - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - dev: false - - /readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - dev: false - - /reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - dev: false - - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: false - - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - dev: false - - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - dev: false - - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: true - - /reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: false - - /roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - dependencies: - boolean: 3.2.0 - detect-node: 2.1.0 - globalthis: 1.0.4 - json-stringify-safe: 5.0.1 - semver-compare: 1.0.0 - sprintf-js: 1.1.3 - dev: false - - /rollup@4.53.3: - resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.53.3 - '@rollup/rollup-android-arm64': 4.53.3 - '@rollup/rollup-darwin-arm64': 4.53.3 - '@rollup/rollup-darwin-x64': 4.53.3 - '@rollup/rollup-freebsd-arm64': 4.53.3 - '@rollup/rollup-freebsd-x64': 4.53.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 - '@rollup/rollup-linux-arm-musleabihf': 4.53.3 - '@rollup/rollup-linux-arm64-gnu': 4.53.3 - '@rollup/rollup-linux-arm64-musl': 4.53.3 - '@rollup/rollup-linux-loong64-gnu': 4.53.3 - '@rollup/rollup-linux-ppc64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-gnu': 4.53.3 - '@rollup/rollup-linux-riscv64-musl': 4.53.3 - '@rollup/rollup-linux-s390x-gnu': 4.53.3 - '@rollup/rollup-linux-x64-gnu': 4.53.3 - '@rollup/rollup-linux-x64-musl': 4.53.3 - '@rollup/rollup-openharmony-arm64': 4.53.3 - '@rollup/rollup-win32-arm64-msvc': 4.53.3 - '@rollup/rollup-win32-ia32-msvc': 4.53.3 - '@rollup/rollup-win32-x64-gnu': 4.53.3 - '@rollup/rollup-win32-x64-msvc': 4.53.3 - fsevents: 2.3.3 - dev: true - - /router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - dev: false - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: false - - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false - - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - requiresBuild: true - - /seedrandom@3.0.5: - resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} - requiresBuild: true - dev: false - optional: true - - /semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - dev: false - - /semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} - engines: {node: '>=10'} - hasBin: true - - /send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - dev: false - - /serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - dependencies: - type-fest: 0.13.1 - dev: false - - /serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - dev: false - - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - dev: false - - /sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - requiresBuild: true - dependencies: - '@img/colour': 1.0.0 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - dev: false - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - /side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - dev: false - - /side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - dev: false - - /side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - dev: false - - /side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - dev: false - - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true - - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: false - - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true - - /simsimd@6.5.5: - resolution: {integrity: sha512-iMZLxr/rWOOhg+L0QimMMvehntzJrPMLizvOXr3VZtQHazskaIRuBtui9QtlZf8iXuPFDBwzLwAdrm/J3U/HwA==} - engines: {node: ~10 >=10.20 || >=12.17} - requiresBuild: true - dependencies: - bindings: 1.5.0 - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - optionalDependencies: - benchmark: 2.1.4 - mathjs: 14.9.1 - usearch: 2.21.4 - dev: false - - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - dev: false - - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - dev: true - - /socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - dev: true - - /socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - dev: true - - /source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - dev: true - - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - dev: false - - /ssri@13.0.0: - resolution: {integrity: sha512-yizwGBpbCn4YomB2lzhZqrHLJoqFGXihNbib3ozhqF/cIp5ue+xSmOQrjNasEE62hFxsCcg/V/z23t4n8jMEng==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - minipass: 7.1.2 - dev: true - - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true - - /statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - dev: false - - /std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - dev: true - - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - dev: false - - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - dependencies: - safe-buffer: 5.2.1 - dev: false - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: false - - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true - - /strip-literal@2.1.1: - resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} - dependencies: - js-tokens: 9.0.1 - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: false - - /table-layout@4.1.1: - resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==} - engines: {node: '>=12.17'} - dependencies: - array-back: 6.2.2 - wordwrapjs: 5.1.1 - dev: false - - /tar@7.5.2: - resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} - engines: {node: '>=18'} - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 - - /thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - dependencies: - thenify: 3.3.1 - dev: false - - /thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - dependencies: - any-promise: 1.3.0 - dev: false - - /tiny-emitter@2.1.0: - resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} - requiresBuild: true - dev: false - optional: true - - /tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - dev: true - - /tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - dev: true - - /tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - dev: true - - /tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: false - - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - dev: false - - /ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 24.10.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - dev: true - - /tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - dev: false - - /type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - dev: true - - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - dev: false - - /type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - dev: false - - /typed-function@4.2.2: - resolution: {integrity: sha512-VwaXim9Gp1bngi/q3do8hgttYn2uC3MoT/gfuMWylnj1IeZBUAyPddHZlo1K05BDoj8DYPpMdiHqH1dDYdJf2A==} - engines: {node: '>= 18'} - requiresBuild: true - dev: false - optional: true - - /typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /typical@4.0.0: - resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} - engines: {node: '>=8'} - dev: false - - /typical@7.3.0: - resolution: {integrity: sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==} - engines: {node: '>=12.17'} - dev: false - - /ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - dev: true - - /undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - dev: false - - /undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - /unique-filename@5.0.0: - resolution: {integrity: sha512-2RaJTAvAb4owyjllTfXzFClJ7WsGxlykkPvCr9pA//LD9goVq+m4PPAeBgNodGZ7nSrntT/auWpJ6Y5IFXcfjg==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - unique-slug: 6.0.0 - dev: true - - /unique-slug@6.0.0: - resolution: {integrity: sha512-4Lup7Ezn8W3d52/xBhZBVdx323ckxa7DEvd9kPQHppTkLoJXw6ltrBCyj5pnrxj0qKDxYMJ56CoxNuFCscdTiw==} - engines: {node: ^20.17.0 || >=22.9.0} - dependencies: - imurmurhash: 0.1.4 - dev: true - - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - dev: false - - /usearch@2.21.4: - resolution: {integrity: sha512-AzavmhAfGubKOLdR3S6Rh/6dvgXqxL+6Fzs1fsgKneQG8i7oLX2Gpqsc4EfdSyKb4sQXhavIiKIguMA2R3cRaA==} - engines: {node: ~10 >=10.20 || >=12.17} - requiresBuild: true - dependencies: - bindings: 1.5.0 - node-addon-api: 8.5.0 - node-gyp-build: 4.8.4 - dev: false - optional: true - - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false - - /uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - dev: false - - /v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true - - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - dev: false - - /vite-node@1.6.1(@types/node@24.10.1): - resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - dependencies: - cac: 6.7.14 - debug: 4.4.3 - pathe: 1.1.2 - picocolors: 1.1.1 - vite: 5.4.21(@types/node@24.10.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - - /vite@5.4.21(@types/node@24.10.1): - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - dependencies: - '@types/node': 24.10.1 - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.53.3 - optionalDependencies: - fsevents: 2.3.3 - dev: true - - /vitest@1.6.1(@types/node@24.10.1): - resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.1 - '@vitest/ui': 1.6.1 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - dependencies: - '@types/node': 24.10.1 - '@vitest/expect': 1.6.1 - '@vitest/runner': 1.6.1 - '@vitest/snapshot': 1.6.1 - '@vitest/spy': 1.6.1 - '@vitest/utils': 1.6.1 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.4.3 - execa: 8.0.1 - local-pkg: 0.5.1 - magic-string: 0.30.21 - pathe: 1.1.2 - picocolors: 1.1.1 - std-env: 3.10.0 - strip-literal: 2.1.1 - tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.21(@types/node@24.10.1) - vite-node: 1.6.1(@types/node@24.10.1) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - dev: true - - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - dependencies: - defaults: 1.0.4 - dev: false - - /weak-lru-cache@1.2.2: - resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} - dev: false - - /web-tree-sitter@0.25.10: - resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} - peerDependencies: - '@types/emscripten': ^1.40.0 - peerDependenciesMeta: - '@types/emscripten': - optional: true - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - - /which@6.0.0: - resolution: {integrity: sha512-f+gEpIKMR9faW/JgAgPK1D7mekkFoqbmiwvNzuhsHetni20QSgzg9Vhn0g2JSJkkfehQnqdUAx7/e15qS1lPxg==} - engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true - dependencies: - isexe: 3.1.1 - dev: true - - /why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - dev: true - - /wordwrapjs@5.1.1: - resolution: {integrity: sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==} - engines: {node: '>=12.17'} - dev: false - - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - dev: false - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: false - - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: false - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - - /yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: false - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: false - - /yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - dev: true - - /yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - dev: true - - /zod-to-json-schema@3.25.0(zod@4.1.13): - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} - peerDependencies: - zod: ^3.25 || ^4 - dependencies: - zod: 4.1.13 - dev: false - - /zod@4.1.13: - resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} diff --git a/public/bench.png b/public/bench.png deleted file mode 100644 index 94f56bba..00000000 Binary files a/public/bench.png and /dev/null differ diff --git a/scripts/compare-engines.ts b/scripts/compare-engines.ts deleted file mode 100644 index 22dd4ab8..00000000 --- a/scripts/compare-engines.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { spawnSync } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; - -type RunResult = { - engine: string; - cwd: string; - cmd: string[]; - exitCode: number | null; - signal: NodeJS.Signals | null; - realSec?: number; - userSec?: number; - sysSec?: number; - maxRssKb?: number; - peakRssKb?: number; - stdout: string; - stderr: string; - timeOutput: string; -}; - -type EngineConfig = { - name: string; - bin: string; -}; - -const DEFAULT_DATASET = path.resolve( - __dirname, - "../../opencode-old/packages/opencode/src", -); -const DEFAULT_OLD_BIN = path.resolve( - __dirname, - "../../old-osgrep/dist/index.js", -); -const DEFAULT_NEW_BIN = path.resolve(__dirname, "../dist/index.js"); -const DEFAULT_RUNS = Number.parseInt(process.env.RUNS || "1", 10) || 1; - -function parseTimeOutput(stderr: string) { - const realSec = matchFloat(stderr, /([\d.]+)\s+real/); - const userSec = matchFloat(stderr, /([\d.]+)\s+user/); - const sysSec = matchFloat(stderr, /([\d.]+)\s+sys/); - const maxRssKb = matchInt( - stderr, - /^\s*([\d]+)\s+maximum resident set size/im, - ); - const peakRssKb = matchInt(stderr, /^\s*([\d]+)\s+peak memory footprint/im); - return { realSec, userSec, sysSec, maxRssKb, peakRssKb }; -} - -function matchFloat(text: string, re: RegExp) { - const m = text.match(re); - return m ? Number.parseFloat(m[1]) : undefined; -} - -function matchInt(text: string, re: RegExp) { - const m = text.match(re); - return m ? Number.parseInt(m[1], 10) : undefined; -} - -function runTimedIndex( - engine: EngineConfig, - cwd: string, - env: NodeJS.ProcessEnv, -): RunResult { - const cmd = [engine.bin, "index", "--reset"]; - const timeCmd = ["/usr/bin/time", "-l", "node", ...cmd]; - const proc = spawnSync(timeCmd[0], timeCmd.slice(1), { - cwd, - env, - encoding: "utf-8", - maxBuffer: 10 * 1024 * 1024, - }); - - const parsed = parseTimeOutput(proc.stderr ?? ""); - - return { - engine: engine.name, - cwd, - cmd, - exitCode: proc.status, - signal: proc.signal, - ...parsed, - stdout: proc.stdout ?? "", - stderr: proc.stderr ?? "", - timeOutput: proc.stderr ?? "", - }; -} - -function copyDataset(src: string, dest: string) { - fs.rmSync(dest, { recursive: true, force: true }); - fs.cpSync(src, dest, { recursive: true }); -} - -function formatMb(kb?: number) { - if (!kb || Number.isNaN(kb)) return "n/a"; - return `${(kb / 1024).toFixed(1)} MB`; -} - -function ensureDir(dir: string) { - fs.mkdirSync(dir, { recursive: true }); -} - -function main() { - const dataset = path.resolve(process.env.DATASET || DEFAULT_DATASET); - const oldBin = path.resolve(process.env.OLD_BIN || DEFAULT_OLD_BIN); - const newBin = path.resolve(process.env.NEW_BIN || DEFAULT_NEW_BIN); - const runs = DEFAULT_RUNS; - - const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "osgrep-bench-")); - const resultsDir = path.resolve(__dirname, "../benchmark/results"); - ensureDir(resultsDir); - - const env: NodeJS.ProcessEnv = { - ...process.env, - OSGREP_WORKER_THREADS: process.env.OSGREP_WORKER_THREADS || "4", - OSGREP_WORKER_TASK_TIMEOUT_MS: - process.env.OSGREP_WORKER_TASK_TIMEOUT_MS || "240000", - OSGREP_LOG_PLAIN: "1", - }; - - const engines: EngineConfig[] = [ - { name: "old", bin: oldBin }, - { name: "new", bin: newBin }, - ]; - - const results: RunResult[] = []; - - engines.forEach((engine) => { - for (let i = 0; i < runs; i += 1) { - const stageDir = path.join(tmpRoot, `${engine.name}-run-${i + 1}`); - copyDataset(dataset, stageDir); - const run = runTimedIndex(engine, stageDir, env); - results.push(run); - console.log( - `[${engine.name}] run ${i + 1}: real=${run.realSec?.toFixed(2) ?? "?"}s rss=${formatMb(run.maxRssKb)} exit=${run.exitCode}`, - ); - } - }); - - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - const summaryPath = path.join(resultsDir, `engine-compare-${timestamp}.json`); - const latestPath = path.join(resultsDir, `engine-compare-latest.json`); - - const payload = { - timestamp, - dataset, - runs, - envOverrides: { - OSGREP_WORKER_THREADS: env.OSGREP_WORKER_THREADS, - OSGREP_WORKER_TASK_TIMEOUT_MS: env.OSGREP_WORKER_TASK_TIMEOUT_MS, - }, - engines, - results, - }; - - fs.writeFileSync(summaryPath, JSON.stringify(payload, null, 2)); - fs.writeFileSync(latestPath, JSON.stringify(payload, null, 2)); - console.log(`Saved results to ${summaryPath}`); - console.log(`Latest alias: ${latestPath}`); - console.log(`Staging kept at: ${tmpRoot}`); -} - -main(); diff --git a/scripts/index-bench.sh b/scripts/index-bench.sh deleted file mode 100755 index 4285f6ae..00000000 --- a/scripts/index-bench.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Simple indexing benchmark harness for macOS. -# - Runs osgrep indexing against a target directory with multiple env configs. -# - Wipes Lance data and meta between runs to keep results comparable. - -TARGET_DIR="${TARGET_DIR:-/Users/ryandonofrio/Desktop/osgrep2/opencode/packages/opencode/src}" -OSGREP_BIN="${OSGREP_BIN:-node dist/index.js}" -LOG_DIR="${LOG_DIR:-./benchmarks}" -LOG_FILE="${LOG_FILE:-${LOG_DIR}/index-bench-$(date +%Y%m%d-%H%M%S).log}" -TIMEOUT_SEC="${TIMEOUT_SEC:-600}" - -mkdir -p "${LOG_DIR}" - -CONFIGS=( - "OSGREP_THREADS=1 OSGREP_WORKER_BATCH_SIZE=8" - "OSGREP_THREADS=1 OSGREP_WORKER_BATCH_SIZE=12" - "OSGREP_THREADS=2 OSGREP_WORKER_BATCH_SIZE=12" - "OSGREP_THREADS=2 OSGREP_WORKER_BATCH_SIZE=16" -) - -clean_state() { - echo "Cleaning ~/.osgrep data/meta..." - rm -rf "${HOME}/.osgrep/data" "${HOME}/.osgrep/meta.json" "${HOME}/.osgrep/meta.json.tmp" -} - -run_one() { - local env_line="$1" - echo "==== ${env_line} ====" | tee -a "${LOG_FILE}" - clean_state - SECONDS=0 - local cmd="${env_line} OSGREP_DEBUG_INDEX=1 OSGREP_PROFILE=1 OSGREP_SKIP_META_SAVE=1 ${OSGREP_BIN} index --path \"${TARGET_DIR}\" --reset" - # /usr/bin/time -l (macOS) for resource stats; falls back to builtin time if unavailable. - if command -v /usr/bin/time >/dev/null 2>&1; then - cmd="/usr/bin/time -l ${cmd}" - else - cmd="time ${cmd}" - fi - # Enforce timeout (perl alarm works on macOS) - perl -e 'alarm shift; exec @ARGV' "${TIMEOUT_SEC}" bash -lc "${cmd}" 2>&1 | tee -a "${LOG_FILE}" - echo "Elapsed: ${SECONDS}s" | tee -a "${LOG_FILE}" - echo | tee -a "${LOG_FILE}" -} - -echo "Benchmarking ${TARGET_DIR}" | tee "${LOG_FILE}" -echo "Log: ${LOG_FILE}" -echo - -for cfg in "${CONFIGS[@]}"; do - run_one "${cfg}" -done - -echo "Done. Results recorded in ${LOG_FILE}" diff --git a/scripts/sync-eval-cases.ts b/scripts/sync-eval-cases.ts deleted file mode 100644 index 3da66016..00000000 --- a/scripts/sync-eval-cases.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; - -const newEvalPath = path.resolve(__dirname, "../src/eval.ts"); -const oldEvalPath = path.resolve(__dirname, "../../old-osgrep/src/eval.ts"); - -function extractCases(content: string, label: string) { - const match = content.match( - /export const cases: EvalCase\[] = ([\s\S]*?)\nconst topK/, - ); - if (!match) { - throw new Error(`Could not find cases array in ${label}`); - } - return match[1].trim(); -} - -function replaceCases(targetContent: string, newCases: string) { - return targetContent.replace( - /export const cases: EvalCase\[] = ([\s\S]*?)\nconst topK/, - `export const cases: EvalCase[] = ${newCases}\nconst topK`, - ); -} - -function main() { - const newEval = fs.readFileSync(newEvalPath, "utf-8"); - const oldEval = fs.readFileSync(oldEvalPath, "utf-8"); - - const newCases = extractCases(newEval, newEvalPath); - const updatedOld = replaceCases(oldEval, newCases); - - fs.writeFileSync(oldEvalPath, updatedOld); - console.log(`Synced eval cases from ${newEvalPath} -> ${oldEvalPath}`); -} - -main(); diff --git a/scripts/verify_skeleton.ts b/scripts/verify_skeleton.ts deleted file mode 100644 index 8c33b888..00000000 --- a/scripts/verify_skeleton.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as path from "path"; -import { VectorDB } from "../src/lib/store/vector-db"; -import { ensureProjectPaths } from "../src/lib/utils/project-root"; - -async function main() { - const root = process.cwd(); - const paths = ensureProjectPaths(root); - const db = new VectorDB(paths.lancedbDir); - - try { - const table = await db.ensureTable(); - const results = await table - .query() - .where("path LIKE '%src/commands/skeleton.ts%' AND is_anchor = true") - .limit(1) - .toArray(); - - console.log(`Found ${results.length} anchor chunks.`); - - for (const r of results) { - console.log(`File: ${r.path}`); - const skel = r["file_skeleton"]; - if (skel && typeof skel === "string" && skel.length > 0) { - console.log("āœ… Has skeleton (" + skel.length + " chars)"); - console.log(skel.substring(0, 50) + "..."); - } else { - console.log("āŒ No skeleton found!"); - } - console.log("---"); - } - } catch (e) { - console.error(e); - } finally { - await db.close(); - } -} - -main(); diff --git a/src/commands/claude-code.ts b/src/commands/agent/claude-code.ts similarity index 100% rename from src/commands/claude-code.ts rename to src/commands/agent/claude-code.ts diff --git a/src/commands/codex.ts b/src/commands/agent/codex.ts similarity index 100% rename from src/commands/codex.ts rename to src/commands/agent/codex.ts diff --git a/src/commands/droid.ts b/src/commands/agent/droid.ts similarity index 100% rename from src/commands/droid.ts rename to src/commands/agent/droid.ts diff --git a/src/commands/mcp.ts b/src/commands/agent/mcp.ts similarity index 94% rename from src/commands/mcp.ts rename to src/commands/agent/mcp.ts index 0276a984..3c6b7788 100644 --- a/src/commands/mcp.ts +++ b/src/commands/agent/mcp.ts @@ -7,9 +7,9 @@ import { ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { Command } from "commander"; -import { initialSync } from "../lib/index/syncer"; -import { ensureSetup } from "../lib/setup/setup-helpers"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; +import { initialSync } from "../../lib/index/syncer"; +import { ensureSetup } from "../../lib/setup/setup-helpers"; +import { ensureProjectPaths, findProjectRoot } from "../../lib/utils/project-root"; export const mcp = new Command("mcp") .description("Start MCP server for osgrep") diff --git a/src/commands/opencode.ts b/src/commands/agent/opencode.ts similarity index 98% rename from src/commands/opencode.ts rename to src/commands/agent/opencode.ts index 82a871d0..b503de38 100644 --- a/src/commands/opencode.ts +++ b/src/commands/agent/opencode.ts @@ -80,9 +80,6 @@ Read the specific line range, not the whole file. # Trace call graph (who calls X, what X calls) osgrep trace handleAuth -# Skeleton of a huge file (to find which ranges to read) -osgrep skeleton src/giant-2000-line-file.ts - # Just file paths when you only need locations osgrep "authentication" --compact diff --git a/src/commands/search.ts b/src/commands/search.ts index 6765db03..d5a27922 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -1,4 +1,3 @@ -import * as fs from "node:fs"; import * as path from "node:path"; import type { Command } from "commander"; import { Command as CommanderCommand } from "commander"; @@ -9,9 +8,12 @@ import { } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; import { Searcher } from "../lib/search/searcher"; +import type { + ExpandedResult, + ExpandOptions, + ExpansionNode, +} from "../lib/search/expansion-types"; import { ensureSetup } from "../lib/setup/setup-helpers"; -import { Skeletonizer } from "../lib/skeleton"; -import { getStoredSkeleton } from "../lib/skeleton/retriever"; import type { ChunkType, FileMetadata, @@ -22,7 +24,116 @@ import { gracefulExit } from "../lib/utils/exit"; import { formatTextResults, type TextResult } from "../lib/utils/formatter"; import { isLocked } from "../lib/utils/lock"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; -import { getServerForProject } from "../lib/utils/server-registry"; +import { getServerForProject, unregisterServer } from "../lib/utils/server-registry"; + +/** + * Get expand options for --deep mode. + * Sweet spot config: callers + symbols at depth 2. + */ +function getDeepExpandOptions(deep: boolean): ExpandOptions | undefined { + if (!deep) return undefined; + + return { + maxDepth: 2, + maxExpanded: 20, + maxTokens: 0, + strategies: ["callers", "symbols"], + }; +} + +/** + * Format expanded results for plain text output. + */ +function formatExpandedPlain( + expanded: ExpandedResult, + projectRoot: string, +): string { + const lines: string[] = []; + + // Stats header + const { stats } = expanded; + lines.push( + `\n--- Expanded Results (${expanded.expanded.length} chunks) ---`, + ); + lines.push( + `symbols: ${stats.symbolsResolved} | callers: ${stats.callersFound} | neighbors: ${stats.neighborsAdded}${ + stats.budgetRemaining !== undefined + ? ` | tokens: ${stats.totalTokens} (${stats.budgetRemaining} remaining)` + : "" + }${expanded.truncated ? " [truncated]" : ""}`, + ); + lines.push(""); + + // Group by relationship type + const byRelation = new Map(); + for (const node of expanded.expanded) { + const key = node.relationship; + if (!byRelation.has(key)) byRelation.set(key, []); + byRelation.get(key)!.push(node); + } + + for (const [relation, nodes] of byRelation) { + lines.push(`[${relation}]`); + for (const node of nodes) { + const rawPath = + typeof (node.chunk.metadata as FileMetadata | undefined)?.path === + "string" + ? ((node.chunk.metadata as FileMetadata).path as string) + : "Unknown"; + const relPath = path.isAbsolute(rawPath) + ? path.relative(projectRoot, rawPath) + : rawPath; + const start = node.chunk.generated_metadata?.start_line ?? 0; + const defined = node.chunk.defined_symbols?.slice(0, 3).join(", ") || ""; + lines.push( + ` ${relPath}:${start + 1} via="${node.via}" d=${node.depth}${ + defined ? ` [${defined}]` : "" + }`, + ); + } + lines.push(""); + } + + return lines.join("\n"); +} + +/** + * Format expanded results in compact TSV format. + */ +function formatExpandedCompact( + expanded: ExpandedResult, + projectRoot: string, +): string { + const lines: string[] = []; + lines.push(`expanded\tcount=${expanded.expanded.length}\ttruncated=${expanded.truncated}`); + lines.push("path\tlines\trelation\tvia\tdepth\tdefined"); + + for (const node of expanded.expanded) { + const rawPath = + typeof (node.chunk.metadata as FileMetadata | undefined)?.path === "string" + ? ((node.chunk.metadata as FileMetadata).path as string) + : "Unknown"; + const relPath = path.isAbsolute(rawPath) + ? path.relative(projectRoot, rawPath) + : rawPath; + const start = node.chunk.generated_metadata?.start_line ?? 0; + const end = node.chunk.generated_metadata?.end_line ?? start; + const defined = node.chunk.defined_symbols?.slice(0, 3).join(",") || "-"; + + lines.push( + [ + relPath, + `${start + 1}-${end + 1}`, + node.relationship, + node.via, + String(node.depth), + defined, + ].join("\t"), + ); + } + + return lines.join("\n"); +} function toTextResults(data: SearchResponse["data"]): TextResult[] { return data.map((r) => { @@ -262,99 +373,10 @@ function formatCompactTable( return formatCompactPretty(hits, projectRoot, query, termWidth, true); } -// Reuse Skeletonizer instance -let globalSkeletonizer: Skeletonizer | null = null; - -async function outputSkeletons( - results: any[], - projectRoot: string, - limit: number, - db?: VectorDB | null, -): Promise { - const seenPaths = new Set(); - const filesToProcess: string[] = []; - - for (const result of results) { - const p = (result.metadata as any)?.path; - if (typeof p === "string" && !seenPaths.has(p)) { - seenPaths.add(p); - filesToProcess.push(p); - if (filesToProcess.length >= limit) break; - } - } - - if (filesToProcess.length === 0) { - console.log("No skeleton matches found."); - return; - } - - // Reuse or init skeletonizer for fallbacks - if (!globalSkeletonizer) { - globalSkeletonizer = new Skeletonizer(); - // Lazy init only if we actually fallback - } - - const skeletonOpts = { includeSummary: true }; - const skeletonResults: Array<{ - file: string; - skeleton: string; - tokens: number; - error?: string; - }> = []; - - for (const relPath of filesToProcess) { - // 1. Try DB cache - if (db) { - const cached = await getStoredSkeleton(db, relPath); - if (cached) { - skeletonResults.push({ - file: relPath, - skeleton: cached, - tokens: Math.ceil(cached.length / 4), // Rough estimate - }); - continue; - } - } - - // 2. Fallback to fresh generation - await globalSkeletonizer.init(); - const absPath = path.resolve(projectRoot, relPath); - if (!fs.existsSync(absPath)) { - skeletonResults.push({ - file: relPath, - skeleton: `// File not found: ${relPath}`, - tokens: 0, - error: "File not found", - }); - continue; - } - - const content = fs.readFileSync(absPath, "utf-8"); - const res = await globalSkeletonizer.skeletonizeFile( - relPath, - content, - skeletonOpts, - ); - skeletonResults.push({ - file: relPath, - skeleton: res.skeleton, - tokens: res.tokenEstimate, - error: res.error, - }); - } - - // Since search doesn't support --json explicitly yet, we just print text. - // But if we ever add it, we have the structure. - for (const res of skeletonResults) { - console.log(res.skeleton); - console.log(""); // Separator - } -} - export const search: Command = new CommanderCommand("search") .description("File pattern searcher") .option( - "-m , --max-count ", + "-m, --max-count ", "The maximum number of results to return (total)", "5", ) @@ -373,6 +395,12 @@ export const search: Command = new CommanderCommand("search") ) .option("--plain", "Disable ANSI colors and use simpler formatting", false) + .option( + "--deep", + "Include related code (callers, definitions) for architectural context", + false, + ) + .option( "-s, --sync", "Syncs the local files to the store before searching", @@ -383,27 +411,22 @@ export const search: Command = new CommanderCommand("search") "Show what would be indexed without actually indexing", false, ) - .option( - "--skeleton", - "Show code skeleton for matching files instead of snippets", - false, - ) .argument("", "The pattern to search for") .argument("[path]", "The path to search in") .allowUnknownOption(true) .allowExcessArguments(true) .action(async (pattern, exec_path, _options, cmd) => { const options: { - m: string; + maxCount: string; content: boolean; perFile: string; scores: boolean; minScore: string; compact: boolean; plain: boolean; + deep: boolean; sync: boolean; dryRun: boolean; - skeleton: boolean; } = cmd.optsWithGlobals(); if (exec_path?.startsWith("--")) { @@ -411,6 +434,9 @@ export const search: Command = new CommanderCommand("search") } const root = process.cwd(); + const limitRaw = Number.parseInt(options.maxCount, 10); + const limit = + Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 5; const minScore = Number.isFinite(Number.parseFloat(options.minScore)) ? Number.parseFloat(options.minScore) : 0; @@ -422,94 +448,182 @@ export const search: Command = new CommanderCommand("search") findProjectRoot(execPathForServer) ?? execPathForServer; const server = getServerForProject(projectRootForServer); - if (server) { - try { - const response = await fetch(`http://localhost:${server.port}/search`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query: pattern, - limit: parseInt(options.m, 10), - path: exec_path - ? path.relative(projectRootForServer, path.resolve(exec_path)) - : undefined, - }), - }); - - if (response.ok) { - const body = (await response.json()) as { results: any[] }; - - const searchResult = { data: body.results }; - const filteredData = searchResult.data.filter( - (r) => typeof r.score !== "number" || r.score >= minScore, - ); + if (process.env.DEBUG_SERVER) { + console.error(`[search] projectRootForServer: ${projectRootForServer}`); + console.error(`[search] server found: ${JSON.stringify(server)}`); + } - if (options.skeleton) { - await outputSkeletons( - filteredData, - projectRootForServer, - parseInt(options.m, 10), - // Server doesn't easily expose DB instance here in HTTP client mode, - // but we are in client. Wait, this text implies "Server Search" block. - // Client talks to server. The server returns JSON. - // We don't have DB access here. - // So we pass null, and it will fallback to generating local skeleton (if file exists locally). - // This is acceptable for Phase 3. - null, + if (server && Number.isFinite(server.port) && server.port > 0) { + if (process.env.DEBUG_SERVER) { + console.error(`[search] attempting fetch to server on port ${server.port}`); + } + try { + // Fast preflight so a hung server doesn't add multi-second latency. + const healthTimeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_HEALTH_TIMEOUT_MS || "", + 10, + ); + const healthTimeoutMs = + Number.isFinite(healthTimeoutMsRaw) && healthTimeoutMsRaw > 0 + ? healthTimeoutMsRaw + : process.stdout.isTTY + ? 250 + : 150; + { + const ac = new AbortController(); + let timeout: NodeJS.Timeout | undefined; + try { + timeout = setTimeout(() => ac.abort(), healthTimeoutMs); + const health = await fetch( + `http://localhost:${server.port}/health`, + { signal: ac.signal }, ); - return; + if (!health.ok) throw new Error(`health_${health.status}`); + } finally { + if (timeout) clearTimeout(timeout); } + } - const compactHits = options.compact - ? toCompactHits(filteredData) - : []; - - if (options.compact) { - const compactText = compactHits.length - ? formatCompactTable(compactHits, projectRootForServer, pattern, { - isTTY: !!process.stdout.isTTY, - plain: !!options.plain, - }) - : "No matches found."; - console.log(compactText); - return; // EXIT - } + const timeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_TIMEOUT_MS || "", + 10, + ); + const timeoutMs = + Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 + ? timeoutMsRaw + : process.stdout.isTTY + ? 1200 + : 700; + const ac = new AbortController(); + let timeout: NodeJS.Timeout | undefined; + try { + timeout = setTimeout(() => ac.abort(), timeoutMs); + + const response = await fetch( + `http://localhost:${server.port}/search`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + query: pattern, + limit, + path: exec_path + ? path.relative(projectRootForServer, path.resolve(exec_path)) + : undefined, + deep: options.deep, + }), + signal: ac.signal, + }, + ); - if (!filteredData.length) { - console.log("No matches found."); - return; // EXIT + if (process.env.DEBUG_SERVER) { + console.error(`[search] server response status: ${response.status}`); } + if (response.ok) { + if (process.env.DEBUG_SERVER) { + console.error("[search] server returned OK, using server results"); + } + const body = (await response.json()) as { + results: any[]; + partial?: boolean; + initialSync?: { + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + }; + expanded?: ExpandedResult; + }; + + // Show warning if results are partial (server still indexing) + if (body.partial && body.initialSync) { + const { filesProcessed, filesIndexed } = body.initialSync; + console.warn( + `āš ļø Index building (${filesProcessed} files processed, ${filesIndexed} indexed). Results may be incomplete.`, + ); + } + + const searchResult = { data: body.results }; + const filteredData = searchResult.data.filter( + (r) => typeof r.score !== "number" || r.score >= minScore, + ); - const isTTY = process.stdout.isTTY; - const shouldBePlain = options.plain || !isTTY; - - if (shouldBePlain) { - const mappedResults = toTextResults(filteredData); - const output = formatTextResults( - mappedResults, - pattern, - projectRootForServer, - { - isPlain: true, - compact: options.compact, + const compactHits = options.compact + ? toCompactHits(filteredData) + : []; + + if (options.compact) { + const compactText = compactHits.length + ? formatCompactTable( + compactHits, + projectRootForServer, + pattern, + { + isTTY: !!process.stdout.isTTY, + plain: !!options.plain, + }, + ) + : "No matches found."; + console.log(compactText); + // Add expanded results in compact format + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(""); + console.log(formatExpandedCompact(body.expanded, projectRootForServer)); + } + return; // EXIT + } + + if (!filteredData.length) { + console.log("No matches found."); + return; // EXIT + } + + const isTTY = process.stdout.isTTY; + const shouldBePlain = options.plain || !isTTY; + + if (shouldBePlain) { + const mappedResults = toTextResults(filteredData); + const output = formatTextResults( + mappedResults, + pattern, + projectRootForServer, + { + isPlain: true, + compact: options.compact, + content: options.content, + perFile: parseInt(options.perFile, 10), + showScores: options.scores, + }, + ); + console.log(output); + // Add expanded results + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(formatExpandedPlain(body.expanded, projectRootForServer)); + } + } else { + const { formatResults } = await import("../lib/output/formatter"); + const output = formatResults(filteredData, projectRootForServer, { content: options.content, - perFile: parseInt(options.perFile, 10), - showScores: options.scores, - }, - ); - console.log(output); - } else { - const { formatResults } = await import("../lib/output/formatter"); - const output = formatResults(filteredData, projectRootForServer, { - content: options.content, - }); - console.log(output); + }); + console.log(output); + // Add expanded results in plain format + if (body.expanded && body.expanded.expanded.length > 0) { + console.log(formatExpandedPlain(body.expanded, projectRootForServer)); + } + } + + return; // EXIT successful server search } - - return; // EXIT successful server search + } finally { + if (timeout) clearTimeout(timeout); } } catch (e) { - if (process.env.DEBUG) { + // If the server isn't reachable, remove the stale registry entry so we + // don't pay this timeout cost on every query. + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("ECONNREFUSED") || msg.includes("health_")) { + unregisterServer(server.pid); + } + if (process.env.DEBUG || process.env.DEBUG_SERVER) { console.error( "[search] server request failed, falling back to local:", e, @@ -518,8 +632,20 @@ export const search: Command = new CommanderCommand("search") } } + if (process.env.DEBUG_SERVER) { + console.error("[search] falling through to local search"); + } + try { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + const t = (label: string) => DEBUG_TIMING && console.time(`[cmd] ${label}`); + const te = (label: string) => DEBUG_TIMING && console.timeEnd(`[cmd] ${label}`); + + t("total-cmd"); + t("ensureSetup"); await ensureSetup(); + te("ensureSetup"); + const searchRoot = exec_path ? path.resolve(exec_path) : root; const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; const paths = ensureProjectPaths(projectRoot); @@ -527,7 +653,9 @@ export const search: Command = new CommanderCommand("search") // Propagate project root to worker processes process.env.OSGREP_PROJECT_ROOT = projectRoot; + t("VectorDB-init"); vectorDb = new VectorDB(paths.lancedbDir); + te("VectorDB-init"); // Check for active indexing lock and warn if present // This allows agents (via shim) to know results might be partial. @@ -537,7 +665,9 @@ export const search: Command = new CommanderCommand("search") ); } + t("hasAnyRows"); const hasRows = await vectorDb.hasAnyRows(); + te("hasAnyRows"); const needsSync = options.sync || !hasRows; if (needsSync) { @@ -599,29 +729,32 @@ export const search: Command = new CommanderCommand("search") } const searcher = new Searcher(vectorDb); + const expandOpts = getDeepExpandOptions(options.deep); + t("searcher.search"); const searchResult = await searcher.search( pattern, - parseInt(options.m, 10), + limit, { rerank: true }, undefined, exec_path ? path.relative(projectRoot, path.resolve(exec_path)) : "", ); + te("searcher.search"); + + // Expand results if requested (Phase 1-4 expansion) + let expanded: ExpandedResult | undefined; + if (expandOpts && searchResult.data.length > 0) { + t("searcher.expand"); + expanded = await searcher.expand(searchResult.data, pattern, expandOpts); + te("searcher.expand"); + } + + te("total-cmd"); const filteredData = searchResult.data.filter( (r) => typeof r.score !== "number" || r.score >= minScore, ); - if (options.skeleton) { - await outputSkeletons( - filteredData, - projectRoot, - parseInt(options.m, 10), - vectorDb, - ); - return; - } - const compactHits = options.compact ? toCompactHits(filteredData) : []; const compactText = options.compact && compactHits.length @@ -640,6 +773,11 @@ export const search: Command = new CommanderCommand("search") if (options.compact) { console.log(compactText); + // Add expanded results in compact format + if (expanded && expanded.expanded.length > 0) { + console.log(""); + console.log(formatExpandedCompact(expanded, projectRoot)); + } return; } @@ -656,6 +794,10 @@ export const search: Command = new CommanderCommand("search") showScores: options.scores, }); console.log(output); + // Add expanded results + if (expanded && expanded.expanded.length > 0) { + console.log(formatExpandedPlain(expanded, projectRoot)); + } } else { // Use new holographic formatter for TTY const { formatResults } = await import("../lib/output/formatter"); @@ -663,6 +805,10 @@ export const search: Command = new CommanderCommand("search") content: options.content, }); console.log(output); + // Add expanded results in plain format (for now) + if (expanded && expanded.expanded.length > 0) { + console.log(formatExpandedPlain(expanded, projectRoot)); + } } } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; diff --git a/src/commands/serve.ts b/src/commands/serve.ts index f24eb166..d2514153 100644 --- a/src/commands/serve.ts +++ b/src/commands/serve.ts @@ -2,15 +2,25 @@ import { spawn } from "node:child_process"; import * as fs from "node:fs"; import * as http from "node:http"; import * as path from "node:path"; +import chokidar from "chokidar"; +import type { FSWatcher } from "chokidar"; import { Command } from "commander"; import { PATHS } from "../config"; import { ensureGrammars } from "../lib/index/grammar-loader"; -import { createIndexingSpinner } from "../lib/index/sync-helpers"; +import { DEFAULT_IGNORE_PATTERNS } from "../lib/index/ignore-patterns"; +import type { InitialSyncProgress } from "../lib/index/sync-helpers"; import { initialSync } from "../lib/index/syncer"; +import { GraphBuilder } from "../lib/graph/graph-builder"; import { Searcher } from "../lib/search/searcher"; +import type { ExpandOptions } from "../lib/search/expansion-types"; +import type { MetaEntry } from "../lib/store/meta-cache"; +import { MetaCache } from "../lib/store/meta-cache"; +import type { VectorRecord } from "../lib/store/types"; import { ensureSetup } from "../lib/setup/setup-helpers"; import { VectorDB } from "../lib/store/vector-db"; import { gracefulExit } from "../lib/utils/exit"; +import { isIndexableFile } from "../lib/utils/file-utils"; +import { acquireWriterLockWithRetry } from "../lib/utils/lock"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; import { getServerForProject, @@ -19,6 +29,7 @@ import { registerServer, unregisterServer, } from "../lib/utils/server-registry"; +import { processFile } from "../lib/workers/orchestrator"; export const serve = new Command("serve") .description("Run osgrep as a background server with live indexing") @@ -37,7 +48,11 @@ export const serve = new Command("serve") // Check if already running const existing = getServerForProject(projectRoot); - if (existing && isProcessRunning(existing.pid)) { + if ( + existing && + existing.pid !== process.pid && + isProcessRunning(existing.pid) + ) { console.log( `Server already running for ${projectRoot} (PID: ${existing.pid}, Port: ${existing.port})`, ); @@ -61,6 +76,18 @@ export const serve = new Command("serve") env: { ...process.env, OSGREP_BACKGROUND: "true" }, }); child.unref(); + + // Ensure the spawned server can be discovered/stopped immediately (even + // before it finishes indexing and starts listening). + if (typeof child.pid === "number") { + registerServer({ + pid: child.pid, + port: 0, + projectRoot, + startTime: Date.now(), + }); + } + console.log(`Started background server (PID: ${child.pid})`); return; } @@ -70,45 +97,395 @@ export const serve = new Command("serve") // Propagate project root to worker processes process.env.OSGREP_PROJECT_ROOT = projectRoot; + // Register early to prevent race conditions where multiple sessions + // spawn servers before any finishes indexing. Port 0 = "starting up". + registerServer({ + pid: process.pid, + port: 0, + projectRoot, + startTime: Date.now(), + }); + try { await ensureSetup(); await ensureGrammars(console.log, { silent: true }); const vectorDb = new VectorDB(paths.lancedbDir); const searcher = new Searcher(vectorDb); + const metaCache = new MetaCache(paths.lmdbPath); + + // Serialize DB writes (and optionally searches) to avoid LanceDB contention. + let dbWriteBarrier: Promise = Promise.resolve(); + let isWriting = false; + + // Track initial sync state for HTTP endpoints + let initialSyncState: { + inProgress: boolean; + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + currentFile: string; + } = { + inProgress: true, + filesProcessed: 0, + filesIndexed: 0, + totalFiles: 0, + currentFile: "Starting...", + }; + + // Live indexing: watch filesystem changes and incrementally update the index. + // Enabled by default for `serve` (can disable with OSGREP_WATCH=0). + const watchEnabled = process.env.OSGREP_WATCH !== "0"; + const watchVerbose = process.env.OSGREP_WATCH_VERBOSE === "1"; + const watchMode = (process.env.OSGREP_WATCH_MODE || "auto").toLowerCase(); + const watchDebounceMsRaw = Number.parseInt( + process.env.OSGREP_WATCH_DEBOUNCE_MS || "", + 10, + ); + const watchDebounceMs = + Number.isFinite(watchDebounceMsRaw) && watchDebounceMsRaw >= 0 + ? watchDebounceMsRaw + : 250; + + const pendingUpserts = new Set(); + const pendingUnlinks = new Set(); + let watchTimer: NodeJS.Timeout | undefined; + let watcher: FSWatcher | null = null; + let nativeWatcher: fs.FSWatcher | null = null; + let didLogWatchFallback = false; + + const shouldIgnoreRelPath = (relPathRaw: string): boolean => { + const rel = relPathRaw.split(path.sep).join("/"); + if (!rel || rel === "." || rel.startsWith("../")) return true; + if (rel === ".git" || rel.startsWith(".git/")) return true; + if (rel === ".osgrep" || rel.startsWith(".osgrep/")) return true; + // Large/irrelevant directories (mirrors DEFAULT_IGNORE_PATTERNS intent). + if (rel === "node_modules" || rel.startsWith("node_modules/")) return true; + if (rel.includes("/node_modules/")) return true; + if (rel === "dist" || rel.startsWith("dist/")) return true; + if (rel.includes("/dist/")) return true; + if (rel === "build" || rel.startsWith("build/")) return true; + if (rel.includes("/build/")) return true; + if (rel === "out" || rel.startsWith("out/")) return true; + if (rel.includes("/out/")) return true; + if (rel === "target" || rel.startsWith("target/")) return true; + if (rel.includes("/target/")) return true; + if (rel === "coverage" || rel.startsWith("coverage/")) return true; + if (rel.includes("/coverage/")) return true; + if (rel === "benchmark" || rel.startsWith("benchmark/")) return true; + if (rel.includes("/benchmark/")) return true; + if (rel === ".idea" || rel.startsWith(".idea/")) return true; + if (rel === ".vscode" || rel.startsWith(".vscode/")) return true; + if (rel.endsWith(".DS_Store")) return true; + return false; + }; - // Only show spinner if not in background (or check isTTY) - // If spawned in background with stdio ignore, console.log goes nowhere. - // But we might want to log to a file in the future. + const scheduleFlush = () => { + if (watchTimer) clearTimeout(watchTimer); + watchTimer = setTimeout(() => { + void flushPending().catch((err) => { + console.error("[serve] live index flush failed:", err); + }); + }, watchDebounceMs); + }; - if (!process.env.OSGREP_BACKGROUND) { - const { spinner, onProgress } = createIndexingSpinner( - projectRoot, - "Indexing before starting server...", - ); - try { - await initialSync({ + const recordUpsert = (absPath: string) => { + const rel = path.relative(projectRoot, absPath); + if (shouldIgnoreRelPath(rel)) return; + pendingUnlinks.delete(rel); + pendingUpserts.add(rel); + scheduleFlush(); + }; + + const recordUnlink = (absPath: string) => { + const rel = path.relative(projectRoot, absPath); + if (shouldIgnoreRelPath(rel)) return; + pendingUpserts.delete(rel); + pendingUnlinks.add(rel); + scheduleFlush(); + }; + + const flushPending = async () => { + if (!watchEnabled) return; + if (pendingUpserts.size === 0 && pendingUnlinks.size === 0) return; + + const upserts = Array.from(pendingUpserts); + const unlinks = Array.from(pendingUnlinks); + pendingUpserts.clear(); + pendingUnlinks.clear(); + + // Phase 1: prepare work outside the writer lock (hashing/embedding can be slow). + type PreparedUpsert = { + relPath: string; + absPath: string; + meta: MetaEntry; + shouldDelete: boolean; + vectorsCount: number; + vectors?: VectorRecord[]; + }; + + const prepared: PreparedUpsert[] = []; + + for (const relPath of upserts) { + const absPath = path.join(projectRoot, relPath); + try { + const stats = await fs.promises.stat(absPath); + if (!isIndexableFile(absPath, stats.size)) { + // If it was previously indexed, treat as deletion; still store meta to avoid rework. + prepared.push({ + relPath, + absPath, + meta: { hash: "", mtimeMs: stats.mtimeMs, size: stats.size }, + shouldDelete: true, + vectorsCount: 0, + }); + continue; + } + + const cached = metaCache.get(relPath); + if ( + cached && + cached.mtimeMs === stats.mtimeMs && + cached.size === stats.size + ) { + continue; + } + + const result = await processFile({ path: relPath, absolutePath: absPath }); + + prepared.push({ + relPath, + absPath, + meta: { hash: result.hash, mtimeMs: result.mtimeMs, size: result.size }, + shouldDelete: result.shouldDelete === true, + vectorsCount: result.vectors.length, + vectors: result.vectors, + }); + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "ENOENT") { + unlinks.push(relPath); + continue; + } + console.error(`[serve] live index: failed to prepare ${relPath}:`, err); + } + } + + const plannedUnlinks = unlinks.length; + const plannedUpserts = prepared.length; + + // Phase 2: apply updates under writer lock; serialize DB operations. + const apply = async () => { + let lock: { release: () => Promise } | null = null; + try { + isWriting = true; + lock = await acquireWriterLockWithRetry(paths.osgrepDir, { + maxRetries: 2, + retryDelayMs: 250, + }); + + if (unlinks.length > 0) { + await vectorDb.deletePaths(unlinks); + for (const relPath of unlinks) { + metaCache.delete(relPath); + } + } + + for (const item of prepared) { + // If the file changed again since we prepared, skip and let the next event handle it. + try { + const stats = await fs.promises.stat(item.absPath); + if ( + stats.mtimeMs !== item.meta.mtimeMs || + stats.size !== item.meta.size + ) { + pendingUpserts.add(item.relPath); + continue; + } + } catch (err) { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "ENOENT") { + pendingUnlinks.add(item.relPath); + } + continue; + } + + await vectorDb.deletePaths([item.relPath]); + if (!item.shouldDelete && item.vectorsCount > 0) { + await vectorDb.insertBatch(item.vectors as VectorRecord[]); + } + metaCache.put(item.relPath, item.meta); + } + + if (watchVerbose && (plannedUnlinks > 0 || plannedUpserts > 0)) { + console.log( + `[serve] live index applied: upserts=${plannedUpserts} unlinks=${plannedUnlinks}`, + ); + } + } finally { + if (lock) await lock.release(); + isWriting = false; + } + }; + + dbWriteBarrier = dbWriteBarrier.then(apply, apply); + await dbWriteBarrier; + + // If we re-queued work due to races, schedule another pass. + if (pendingUpserts.size > 0 || pendingUnlinks.size > 0) { + scheduleFlush(); + } + }; + + if (watchEnabled) { + const startChokidar = (mode: "chokidar" | "poll") => { + const ignored: (string | RegExp)[] = [ + ...DEFAULT_IGNORE_PATTERNS, + "**/.git/**", + "**/.osgrep/**", + ]; + + watcher = chokidar.watch(projectRoot, { + ignored, + ignoreInitial: true, + persistent: true, + usePolling: mode === "poll", + interval: mode === "poll" ? 1000 : undefined, + awaitWriteFinish: { + stabilityThreshold: 200, + pollInterval: 100, + }, + }); + + watcher.on("add", recordUpsert); + watcher.on("change", recordUpsert); + watcher.on("unlink", recordUnlink); + watcher.on("error", async (err) => { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "EMFILE" && mode !== "poll") { + if (!didLogWatchFallback) { + didLogWatchFallback = true; + console.error( + "[serve] watcher hit EMFILE; falling back to polling mode (set OSGREP_WATCH_MODE=poll to force).", + ); + } + try { + await watcher?.close(); + } catch {} + watcher = null; + startChokidar("poll"); + return; + } + console.error("[serve] watcher error:", err); + }); + }; + + const startNative = () => { + nativeWatcher = fs.watch( projectRoot, - onProgress, + { recursive: true }, + (eventType, filename) => { + const name = + typeof filename === "string" + ? filename + : typeof (filename as any)?.toString === "function" + ? String((filename as any).toString("utf-8")) + : ""; + if (!name) return; + const rel = name.split(path.sep).join("/"); + if (shouldIgnoreRelPath(rel)) return; + const absPath = path.join(projectRoot, name); + // "rename" can be add/unlink/move; stat in flush determines outcome. + if (eventType === "rename" || eventType === "change") { + recordUpsert(absPath); + } + }, + ); + nativeWatcher.on("error", (err) => { + const code = (err as NodeJS.ErrnoException)?.code; + if (code === "EMFILE") { + if (!didLogWatchFallback) { + didLogWatchFallback = true; + console.error( + "[serve] native watcher hit EMFILE; falling back to polling mode (set OSGREP_WATCH_MODE=poll to force).", + ); + } + try { + nativeWatcher?.close(); + } catch {} + nativeWatcher = null; + startChokidar("poll"); + return; + } + console.error("[serve] native watcher error:", err); }); + }; + + if (watchMode === "off") { + // noop + } else if (watchMode === "native") { + startNative(); + } else if (watchMode === "poll") { + startChokidar("poll"); + } else if (watchMode === "chokidar") { + startChokidar("chokidar"); + } else { + // auto + if (process.platform === "darwin" || process.platform === "win32") { + startNative(); + } else { + startChokidar("chokidar"); + } + } + } + + // Helper to run initial sync after server is listening + const runInitialSync = async () => { + const onProgress = (info: InitialSyncProgress) => { + initialSyncState.filesProcessed = info.processed; + initialSyncState.filesIndexed = info.indexed; + initialSyncState.totalFiles = info.total; + initialSyncState.currentFile = info.filePath ?? ""; + }; + + try { + await initialSync({ projectRoot, onProgress }); await vectorDb.createFTSIndex(); - spinner.succeed("Initial index ready. Starting server..."); + initialSyncState.inProgress = false; + initialSyncState.currentFile = ""; + + if (!process.env.OSGREP_BACKGROUND) { + console.log("Initial index ready."); + } } catch (e) { - spinner.fail("Indexing failed"); - throw e; + console.error("Initial sync failed:", e); + // Mark as done but leave currentFile as error indicator + initialSyncState.inProgress = false; + initialSyncState.currentFile = "sync_failed"; } - } else { - // In background, just sync quietly - await initialSync({ projectRoot }); - await vectorDb.createFTSIndex(); - } + }; const server = http.createServer(async (req, res) => { try { if (req.method === "GET" && req.url === "/health") { res.statusCode = 200; res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ status: "ok" })); + res.end( + JSON.stringify({ + status: initialSyncState.inProgress ? "initializing" : "ok", + initialSync: initialSyncState.inProgress + ? { + inProgress: true, + filesProcessed: initialSyncState.filesProcessed, + filesIndexed: initialSyncState.filesIndexed, + totalFiles: initialSyncState.totalFiles, + currentFile: initialSyncState.currentFile, + } + : null, + indexing: isWriting, + watch: watchEnabled, + }), + ); return; } @@ -165,32 +542,130 @@ export const serve = new Command("serve") // Add AbortController for cancellation const ac = new AbortController(); - req.on("close", () => { + req.on("aborted", () => { ac.abort(); }); + res.on("close", () => { + if (!res.writableEnded) ac.abort(); + }); - const result = await searcher.search( - query, - limit, - { rerank: true }, - undefined, - searchPath, - undefined, // intent - ac.signal, + const timeoutMsRaw = Number.parseInt( + process.env.OSGREP_SERVER_SEARCH_TIMEOUT_MS || "", + 10, ); + const timeoutMs = + Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 + ? timeoutMsRaw + : 60000; + let timeout: NodeJS.Timeout | undefined; + + try { + timeout = setTimeout(() => { + ac.abort(); + if (!res.headersSent && !res.writableEnded) { + res.statusCode = 504; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "search_timeout" })); + } + }, timeoutMs); + + // Parse deep option from request body + let expandOpts: ExpandOptions | undefined; + if (body.deep === true) { + expandOpts = { + maxDepth: 2, + maxExpanded: 20, + maxTokens: 0, + strategies: ["callers", "symbols"], + }; + } - if (ac.signal.aborted) { - // Request was cancelled, don't write response if possible - // (Though usually 'close' means the socket is gone anyway) - return; - } + const debug = process.env.DEBUG_SERVER === "1"; + if (debug) { + console.log( + `[serve] Starting search for "${query}", indexing=${isWriting} signal.aborted=${ac.signal.aborted}${ + expandOpts ? " deep=true" : "" + }`, + ); + } - res.statusCode = 200; - res.setHeader("Content-Type", "application/json"); - res.end(JSON.stringify({ results: result.data })); + const result = await searcher.search( + query, + limit, + { rerank: true }, + undefined, + searchPath, + ac.signal, + ); + if (debug) { + console.log( + `[serve] Search completed, ${result.data.length} results`, + ); + } + + if (ac.signal.aborted) { + console.log("[serve] Signal aborted after search"); + return; + } + + // Expand results if requested + let expanded; + if (expandOpts && result.data.length > 0) { + if (debug) { + console.log(`[serve] Expanding results with depth=${expandOpts.maxDepth}`); + } + expanded = await searcher.expand(result.data, query, expandOpts); + if (debug) { + console.log( + `[serve] Expansion completed, ${expanded.expanded.length} expanded chunks`, + ); + } + } + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + const response: { + results: typeof result.data; + partial?: boolean; + initialSync?: { + filesProcessed: number; + filesIndexed: number; + totalFiles: number; + }; + expanded?: typeof expanded; + } = { results: result.data }; + + if (initialSyncState.inProgress) { + response.partial = true; + response.initialSync = { + filesProcessed: initialSyncState.filesProcessed, + filesIndexed: initialSyncState.filesIndexed, + totalFiles: initialSyncState.totalFiles, + }; + } + + if (expanded) { + response.expanded = expanded; + } + + res.end(JSON.stringify(response)); + } finally { + if (timeout) clearTimeout(timeout); + } } catch (err) { + console.log(`[serve] Search error: ${err instanceof Error ? err.name + ': ' + err.message : err}`); if (err instanceof Error && err.name === "AbortError") { - // Request cancelled + if (!res.headersSent && !res.writableEnded) { + res.statusCode = 504; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "search_cancelled" })); + } + return; + } + if (isWriting && !res.headersSent && !res.writableEnded) { + res.statusCode = 503; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "indexing_in_progress" })); return; } res.statusCode = 500; @@ -211,6 +686,85 @@ export const serve = new Command("serve") return; } + if (req.method === "POST" && req.url === "/trace") { + const chunks: Buffer[] = []; + let totalSize = 0; + let aborted = false; + + req.on("data", (chunk) => { + if (aborted) return; + totalSize += chunk.length; + if (totalSize > 100_000) { + aborted = true; + res.statusCode = 413; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "payload_too_large" })); + req.destroy(); + return; + } + chunks.push(chunk); + }); + + req.on("end", async () => { + if (aborted) return; + try { + const body = chunks.length + ? JSON.parse(Buffer.concat(chunks).toString("utf-8")) + : {}; + const symbol = typeof body.symbol === "string" ? body.symbol : ""; + + if (!symbol) { + res.statusCode = 400; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "symbol_required" })); + return; + } + + const graphBuilder = new GraphBuilder(vectorDb); + const graph = await graphBuilder.buildGraph(symbol, { + depth: typeof body.depth === "number" ? body.depth : 1, + callersOnly: body.callers === true, + calleesOnly: body.callees === true, + pathPrefix: typeof body.path === "string" ? body.path : undefined, + }); + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ + symbol, + center: graph.center + ? { + file: graph.center.file, + line: graph.center.line, + role: graph.center.role, + } + : null, + callers: graph.callers.map((c) => ({ + symbol: c.symbol, + file: c.file, + line: c.line, + })), + callees: graph.callees, + })); + } catch (err) { + res.statusCode = 500; + res.setHeader("Content-Type", "application/json"); + res.end( + JSON.stringify({ + error: (err as Error)?.message || "trace_failed", + }), + ); + } + }); + + req.on("error", (err) => { + console.error("[serve] trace request error:", err); + aborted = true; + }); + + return; + } + res.statusCode = 404; res.end(); } catch (err) { @@ -252,6 +806,7 @@ export const serve = new Command("serve") console.log( `osgrep server listening on http://localhost:${actualPort} (${projectRoot})`, ); + console.log("Starting initial index..."); } registerServer({ pid: process.pid, @@ -259,11 +814,28 @@ export const serve = new Command("serve") projectRoot, startTime: Date.now(), }); + + // Start initial sync after server is listening (non-blocking) + runInitialSync().catch((err) => { + console.error("Initial sync error:", err); + }); }); const shutdown = async () => { unregisterServer(process.pid); + if (watchTimer) { + clearTimeout(watchTimer); + watchTimer = undefined; + } + try { + await watcher?.close(); + } catch {} + try { + nativeWatcher?.close(); + } catch {} + nativeWatcher = null + // Properly await server close await new Promise((resolve, reject) => { server.close((err) => { @@ -284,12 +856,16 @@ export const serve = new Command("serve") } catch (e) { console.error("Error closing vector DB:", e); } + try { + metaCache.close(); + } catch {} await gracefulExit(); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); } catch (error) { + unregisterServer(process.pid); const message = error instanceof Error ? error.message : "Unknown error"; console.error("Serve failed:", message); process.exitCode = 1; @@ -316,28 +892,67 @@ serve .command("stop") .description("Stop background servers") .option("--all", "Stop all servers", false) - .action((options) => { + .action(async (options) => { + const waitForExit = async (pid: number, timeoutMs: number) => { + const deadline = Date.now() + Math.max(0, timeoutMs); + while (Date.now() < deadline) { + if (!isProcessRunning(pid)) return true; + await new Promise((r) => setTimeout(r, 75)); + } + return !isProcessRunning(pid); + }; + + const stopPid = async (pid: number): Promise => { + try { + // If the process is stopped (job control), SIGTERM won't be handled until resumed. + process.kill(pid, "SIGCONT"); + } catch {} + + try { + process.kill(pid, "SIGTERM"); + } catch (e) { + unregisterServer(pid); + return false; + } + + const exited = await waitForExit(pid, 2000); + if (exited) { + unregisterServer(pid); + return true; + } + + try { + process.kill(pid, "SIGKILL"); + } catch (e) { + unregisterServer(pid); + return false; + } + + const killed = await waitForExit(pid, 2000); + unregisterServer(pid); + return killed; + }; + if (options.all) { const servers = listServers(); let count = 0; - servers.forEach((s) => { - try { - process.kill(s.pid, "SIGTERM"); - count++; - } catch (e) { - console.error(`Failed to stop PID ${s.pid}:`, e); - } - }); + for (const s of servers) { + const ok = await stopPid(s.pid); + if (ok) count++; + else console.error(`Failed to stop PID ${s.pid}`); + } console.log(`Stopped ${count} servers.`); } else { const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); const server = getServerForProject(projectRoot); if (server) { - try { - process.kill(server.pid, "SIGTERM"); - console.log(`Stopped server for ${projectRoot} (PID: ${server.pid})`); - } catch (e) { - console.error(`Failed to stop PID ${server.pid}:`, e); + const ok = await stopPid(server.pid); + if (ok) { + console.log( + `Stopped server for ${projectRoot} (PID: ${server.pid})`, + ); + } else { + console.error(`Failed to stop PID ${server.pid}`); } } else { console.log(`No server found for ${projectRoot}`); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index eebb528f..13119e8a 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; -import * as path from "node:path"; import { Command } from "commander"; -import { MODEL_IDS, PATHS } from "../config"; +import { PATHS } from "../config"; import { ensureGrammars } from "../lib/index/grammar-loader"; +import { initNative } from "../lib/native"; import { ensureSetup } from "../lib/setup/setup-helpers"; import { gracefulExit } from "../lib/utils/exit"; @@ -21,8 +21,6 @@ export const setup = new Command("setup") // Show final status console.log("\nSetup Complete!\n"); - const modelIds = [MODEL_IDS.embed, MODEL_IDS.colbert]; - const checkDir = (name: string, p: string) => { const exists = fs.existsSync(p); const symbol = exists ? "āœ“" : "āœ—"; @@ -30,54 +28,16 @@ export const setup = new Command("setup") }; checkDir("Global Root", PATHS.globalRoot); - checkDir("Models", PATHS.models); checkDir("Grammars", PATHS.grammars); // Download Grammars console.log("\nChecking Tree-sitter Grammars..."); await ensureGrammars(); - const modelStatuses = modelIds.map((id) => { - const modelPath = path.join(PATHS.models, ...id.split("/")); - return { id, path: modelPath, exists: fs.existsSync(modelPath) }; - }); - - modelStatuses.forEach(({ id, exists }) => { - const symbol = exists ? "āœ“" : "āœ—"; - console.log(`${symbol} Model: ${id}`); - }); - - // Check for skiplist.json and try to download if missing - const colbertPath = path.join( - PATHS.models, - ...MODEL_IDS.colbert.split("/"), - ); - const skiplistPath = path.join(colbertPath, "skiplist.json"); - if (fs.existsSync(skiplistPath)) { - console.log(`āœ“ Skiplist found: ${skiplistPath}`); - } else { - console.log(`⚠ Skiplist missing, attempting to download...`); - try { - const url = `https://huggingface.co/${MODEL_IDS.colbert}/resolve/main/skiplist.json`; - const response = await fetch(url); - if (response.ok) { - const buffer = await response.arrayBuffer(); - fs.writeFileSync(skiplistPath, Buffer.from(buffer)); - console.log(`āœ“ Skiplist downloaded successfully`); - } else { - console.log( - `⚠ Skiplist download failed (HTTP ${response.status}), will use fallback`, - ); - console.log(` Expected at: ${skiplistPath}`); - } - } catch (error) { - console.log(`⚠ Skiplist download failed, will use fallback`); - console.log( - ` Error: ${error instanceof Error ? error.message : String(error)}`, - ); - console.log(` Expected at: ${skiplistPath}`); - } - } + // Pre-warm native models (downloads via HuggingFace Hub cache on first run) + console.log("\nInitializing native models..."); + await initNative(); + console.log("āœ“ Native models ready"); console.log(`\nosgrep is ready! You can now run:`); console.log(` osgrep index # Index your repository`); diff --git a/src/commands/skeleton.ts b/src/commands/skeleton.ts deleted file mode 100644 index b24e9bbb..00000000 --- a/src/commands/skeleton.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * osgrep skeleton - Show code skeleton (signatures without implementation) - * - * Usage: - * osgrep skeleton # Skeleton of a file - * osgrep skeleton # Find symbol and skeleton its file - * osgrep skeleton "query" # Search and skeleton top results - */ - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { Command } from "commander"; -import { createIndexingSpinner } from "../lib/index/sync-helpers"; -import { initialSync } from "../lib/index/syncer"; -import { Searcher } from "../lib/search/searcher"; -import { ensureSetup } from "../lib/setup/setup-helpers"; -import { getStoredSkeleton } from "../lib/skeleton/retriever"; -import { Skeletonizer } from "../lib/skeleton/skeletonizer"; -import { VectorDB } from "../lib/store/vector-db"; -import { gracefulExit } from "../lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; - -interface SkeletonOptions { - limit: string; - json: boolean; - noSummary: boolean; - sync: boolean; -} - -/** - * Check if target looks like a file path. - */ -function isFilePath(target: string): boolean { - // Has path separator or file extension - return ( - target.includes("/") || target.includes("\\") || /\.\w{1,10}$/.test(target) - ); -} - -/** - * Check if target looks like a symbol name (PascalCase or camelCase identifier). - */ -function isSymbolLike(target: string): boolean { - // PascalCase class name or camelCase function name - // Must be a single word without spaces - return /^[A-Za-z_][A-Za-z0-9_]*$/.test(target) && !target.includes(" "); -} - -/** - * Find a file by symbol name in the index. - */ -async function findFileBySymbol( - symbol: string, - db: VectorDB, -): Promise { - try { - const table = await db.ensureTable(); - - // Search for files that define this symbol - const results = await table.search(symbol).limit(10).toArray(); - - // Find a result where this symbol is defined - for (const result of results) { - const defined = result.defined_symbols as string[] | undefined; - if (defined?.includes(symbol)) { - return result.path as string; - } - } - - // Fallback: just return the first match's file - if (results.length > 0) { - return results[0].path as string; - } - - return null; - } catch { - return null; - } -} - -export const skeleton = new Command("skeleton") - .description("Show code skeleton (signatures without implementation)") - .argument("", "File path, symbol name, or search query") - .option("-l, --limit ", "Max files for query mode", "3") - .option("--json", "Output as JSON", false) - .option("--no-summary", "Omit call/complexity summary in bodies", false) - .option("-s, --sync", "Sync index before searching", false) - .action(async (target: string, options: SkeletonOptions, _cmd) => { - let vectorDb: VectorDB | null = null; - - try { - // Initialize - await ensureSetup(); - const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); - const paths = ensureProjectPaths(projectRoot); - vectorDb = new VectorDB(paths.lancedbDir); - - // Sync if requested - if (options.sync) { - const { spinner, onProgress } = createIndexingSpinner( - projectRoot, - "Syncing...", - { verbose: false }, - ); - await initialSync({ projectRoot, onProgress }); - spinner.succeed("Sync complete"); - } - - // Initialize skeletonizer - const skeletonizer = new Skeletonizer(); - await skeletonizer.init(); - - const skeletonOpts = { - includeSummary: !options.noSummary, - }; - - // Determine mode based on target - if (isFilePath(target)) { - // === FILE MODE === - const filePath = path.resolve(target); - - if (!fs.existsSync(filePath)) { - console.error(`File not found: ${filePath}`); - process.exitCode = 1; - return; - } - - if (vectorDb) { - const relativeToProject = path.relative(projectRoot, filePath); - const cached = await getStoredSkeleton(vectorDb, relativeToProject); - if (cached) { - outputResult( - { - success: true, - skeleton: cached, - tokenEstimate: Math.ceil(cached.length / 4), - }, - options, - ); - return; - } - } - - const content = fs.readFileSync(filePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - outputResult(result, options); - } else if (isSymbolLike(target) && !target.includes(" ")) { - // === SYMBOL MODE === - const filePath = await findFileBySymbol(target, vectorDb); - - if (!filePath) { - console.error(`Symbol not found in index: ${target}`); - console.error( - "Try running 'osgrep index' first or use a search query.", - ); - process.exitCode = 1; - return; - } - - const absolutePath = path.resolve(projectRoot, filePath); - if (!fs.existsSync(absolutePath)) { - console.error(`File not found: ${absolutePath}`); - process.exitCode = 1; - return; - } - - const cached = await getStoredSkeleton(vectorDb!, filePath); - if (cached) { - outputResult( - { - success: true, - skeleton: cached, - tokenEstimate: Math.ceil(cached.length / 4), - }, - options, - ); - return; - } - - const content = fs.readFileSync(absolutePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - outputResult(result, options); - } else { - // === QUERY MODE === - const searcher = new Searcher(vectorDb); - const limit = Math.min(Number.parseInt(options.limit, 10) || 3, 10); - - const searchResults = await searcher.search(target, limit); - - if (!searchResults.data || searchResults.data.length === 0) { - console.error(`No results found for: ${target}`); - process.exitCode = 1; - return; - } - - // Get unique file paths from results - const seenPaths = new Set(); - const filePaths: string[] = []; - - for (const result of searchResults.data) { - const resultPath = (result.metadata as { path?: string })?.path; - if (resultPath && !seenPaths.has(resultPath)) { - seenPaths.add(resultPath); - filePaths.push(resultPath); - if (filePaths.length >= limit) break; - } - } - - // Skeletonize each file - const results: Array<{ - file: string; - skeleton: string; - tokens: number; - error?: string; - }> = []; - - for (const filePath of filePaths) { - const absolutePath = path.resolve(projectRoot, filePath); - - if (!fs.existsSync(absolutePath)) { - results.push({ - file: filePath, - skeleton: `// File not found: ${filePath}`, - tokens: 0, - error: "File not found", - }); - continue; - } - - // Try cache first - const cached = await getStoredSkeleton(vectorDb!, filePath); - if (cached) { - results.push({ - file: filePath, - skeleton: cached, - tokens: Math.ceil(cached.length / 4), - }); - continue; - } - - const content = fs.readFileSync(absolutePath, "utf-8"); - const result = await skeletonizer.skeletonizeFile( - filePath, - content, - skeletonOpts, - ); - - results.push({ - file: filePath, - skeleton: result.skeleton, - tokens: result.tokenEstimate, - error: result.error, - }); - } - - // Output results - if (options.json) { - console.log(JSON.stringify(results, null, 2)); - } else { - for (const result of results) { - console.log(result.skeleton); - console.log(""); // Blank line between files - } - } - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.error("Error:", message); - process.exitCode = 1; - } finally { - if (vectorDb) { - try { - await vectorDb.close(); - } catch { - // Ignore close errors - } - } - const code = typeof process.exitCode === "number" ? process.exitCode : 0; - await gracefulExit(code); - } - }); - -/** - * Output a skeleton result. - */ -function outputResult( - result: { - success: boolean; - skeleton: string; - tokenEstimate: number; - error?: string; - }, - options: SkeletonOptions, -): void { - if (options.json) { - console.log( - JSON.stringify( - { - success: result.success, - skeleton: result.skeleton, - tokens: result.tokenEstimate, - error: result.error, - }, - null, - 2, - ), - ); - } else { - console.log(result.skeleton); - } -} diff --git a/src/commands/trace.ts b/src/commands/trace.ts index 7a52032d..28ac9572 100644 --- a/src/commands/trace.ts +++ b/src/commands/trace.ts @@ -1,38 +1,278 @@ import { Command } from "commander"; -import { GraphBuilder } from "../lib/graph/graph-builder"; -import { formatTrace } from "../lib/output/formatter"; +import { GraphBuilder, type CallGraph } from "../lib/graph/graph-builder"; import { VectorDB } from "../lib/store/vector-db"; import { gracefulExit } from "../lib/utils/exit"; import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; +const style = { + bold: (s: string) => `\x1b[1m${s}\x1b[22m`, + dim: (s: string) => `\x1b[2m${s}\x1b[22m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[39m`, + green: (s: string) => `\x1b[32m${s}\x1b[39m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[39m`, +}; + +/** + * Dedupe and count callees. Returns "foo bar baz" or "foo bar (x3) baz" for repeats. + */ +function formatCallees(callees: string[]): string { + const counts = new Map(); + for (const c of callees) { + counts.set(c, (counts.get(c) || 0) + 1); + } + return [...counts.entries()] + .map(([name, count]) => (count > 1 ? `${name} (x${count})` : name)) + .join(" "); +} + +/** + * Group callers by file. Returns map of file -> line numbers. + */ +function groupCallersByFile( + callers: Array<{ file: string; line: number; symbol: string }>, +): Map { + const byFile = new Map(); + for (const c of callers) { + const lines = byFile.get(c.file) || []; + lines.push(c.line); + byFile.set(c.file, lines); + } + // Sort lines within each file + for (const lines of byFile.values()) { + lines.sort((a, b) => a - b); + } + return byFile; +} + +/** + * Count total items for format decision. + */ +function countItems(graph: CallGraph): number { + const uniqueCallees = new Set(graph.callees).size; + const callerFiles = groupCallersByFile(graph.callers).size; + return uniqueCallees + callerFiles; +} + +/** + * Format call graph for minimal agent-friendly output. + * Uses tree format for small results, compact grouped format for large. + */ +function formatPlain(graph: CallGraph, symbol: string): string { + const itemCount = countItems(graph); + + // Use tree format for small results (cleaner), compact for large + if (itemCount <= 10) { + return formatPlainTree(graph, symbol); + } + return formatPlainCompact(graph, symbol); +} + +/** + * Tree format for small results - cleaner to read. + */ +function formatPlainTree(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + + if (graph.center) { + lines.push(`${symbol} (${graph.center.file}:${graph.center.line})`); + } else { + lines.push(`${symbol} (not found)`); + } + + const hasCallees = graph.callees.length > 0; + const hasCallers = graph.callers.length > 0; + + if (hasCallees) { + const branch = hasCallers ? "ā”œā”€ā”€" : "└──"; + lines.push(`${branch} calls:`); + + // Dedupe callees + const counts = new Map(); + for (const c of graph.callees) { + counts.set(c, (counts.get(c) || 0) + 1); + } + const calleeList = [...counts.entries()]; + + calleeList.forEach(([name, count], i) => { + const prefix = hasCallers ? "│ " : " "; + const sym = i === calleeList.length - 1 ? "└──" : "ā”œā”€ā”€"; + const countStr = count > 1 ? ` (x${count})` : ""; + lines.push(`${prefix}${sym} ${name}${countStr}`); + }); + } + + if (hasCallers) { + lines.push("└── called by:"); + const byFile = groupCallersByFile(graph.callers); + const files = [...byFile.entries()]; + + files.forEach(([file, fileLines], i) => { + const sym = i === files.length - 1 ? "└──" : "ā”œā”€ā”€"; + const lineStr = + fileLines.length === 1 + ? `line ${fileLines[0]}` + : `lines ${fileLines.join(", ")}`; + lines.push(` ${sym} ${file}: ${lineStr}`); + }); + } + + if (!hasCallees && !hasCallers) { + lines.push(" (no callers or callees found)"); + } + + return lines.join("\n"); +} + +/** + * Compact grouped format for large results. + */ +function formatPlainCompact(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + lines.push(symbol); + + if (graph.center) { + lines.push(` def: ${graph.center.file}:${graph.center.line}`); + } else { + lines.push(" def: (not found)"); + } + + if (graph.callees.length > 0) { + lines.push(` calls: ${formatCallees(graph.callees)}`); + } + + if (graph.callers.length > 0) { + lines.push(" called_by:"); + const byFile = groupCallersByFile(graph.callers); + for (const [file, fileLines] of byFile) { + const lineStr = + fileLines.length === 1 + ? `line ${fileLines[0]}` + : `lines ${fileLines.join(", ")}`; + lines.push(` ${file}: ${lineStr}`); + } + } + + return lines.join("\n"); +} + +/** + * Format call graph as a tree for human-readable output. + */ +function formatTree(graph: CallGraph, symbol: string): string { + const lines: string[] = []; + + if (graph.center) { + const role = graph.center.role ? ` ${style.dim(graph.center.role)}` : ""; + lines.push( + `${style.bold(symbol)} (${style.cyan(`${graph.center.file}:${graph.center.line}`)})${role}`, + ); + } else { + lines.push(`${style.bold(symbol)} ${style.dim("(definition not found)")}`); + } + + const hasCallees = graph.callees.length > 0; + const hasCallers = graph.callers.length > 0; + + if (hasCallees) { + const branch = hasCallers ? "\u251c\u2500\u2500" : "\u2514\u2500\u2500"; + lines.push(`${branch} ${style.yellow("calls:")}`); + graph.callees.forEach((callee, i) => { + const prefix = hasCallers ? "\u2502 " : " "; + const sym = + i === graph.callees.length - 1 + ? "\u2514\u2500\u2500" + : "\u251c\u2500\u2500"; + lines.push(`${prefix}${sym} ${callee}`); + }); + } + + if (hasCallers) { + lines.push(`\u2514\u2500\u2500 ${style.green("called by:")}`); + graph.callers.forEach((caller, i) => { + const sym = + i === graph.callers.length - 1 + ? "\u2514\u2500\u2500" + : "\u251c\u2500\u2500"; + lines.push( + ` ${sym} ${caller.symbol} (${style.cyan(`${caller.file}:${caller.line}`)})`, + ); + }); + } + + if (!hasCallees && !hasCallers) { + lines.push(style.dim(" (no callers or callees found)")); + } + + return lines.join("\n"); +} + +/** + * Format call graph as JSON for programmatic use. + */ +function formatJson(graph: CallGraph, symbol: string): string { + return JSON.stringify( + { + symbol, + center: graph.center + ? { + file: graph.center.file, + line: graph.center.line, + role: graph.center.role, + } + : null, + callers: graph.callers.map((c) => ({ + symbol: c.symbol, + file: c.file, + line: c.line, + })), + callees: graph.callees, + }, + null, + 2, + ); +} + export const trace = new Command("trace") - .description("Trace the call graph for a symbol") - .argument("", "The symbol to trace") - .action(async (symbol) => { - const root = process.cwd(); - let vectorDb: VectorDB | null = null; + .description("Show call graph for a symbol (callers and callees)") + .argument("", "Symbol name to trace") + .option("-d, --depth ", "Traversal depth (default: 1)", "1") + .option("--callers", "Show only callers (who calls this)") + .option("--callees", "Show only callees (what this calls)") + .option("-p, --path ", "Filter to path prefix") + .option("--pretty", "Pretty tree output (default for TTY)") + .option("--plain", "Plain minimal output (default for non-TTY)") + .option("--json", "JSON output") + .action(async (symbol, cmd) => { + const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd(); + const paths = ensureProjectPaths(projectRoot); + const db = new VectorDB(paths.lancedbDir); try { - const projectRoot = findProjectRoot(root) ?? root; - const paths = ensureProjectPaths(projectRoot); - - vectorDb = new VectorDB(paths.lancedbDir); - - const graphBuilder = new GraphBuilder(vectorDb); - const graph = await graphBuilder.buildGraph(symbol); - console.log(formatTrace(graph)); - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - console.error("Trace failed:", message); - process.exitCode = 1; - } finally { - if (vectorDb) { - try { - await vectorDb.close(); - } catch (err) { - console.error("Failed to close VectorDB:", err); - } + const builder = new GraphBuilder(db); + + const graph = await builder.buildGraph(symbol, { + depth: Number.parseInt(cmd.depth, 10) || 1, + callersOnly: cmd.callers as boolean, + calleesOnly: cmd.callees as boolean, + pathPrefix: cmd.path as string | undefined, + }); + + // Determine output format + let output: string; + if (cmd.json) { + output = formatJson(graph, symbol); + } else if (cmd.pretty) { + output = formatTree(graph, symbol); + } else if (cmd.plain || !process.stdout.isTTY) { + output = formatPlain(graph, symbol); + } else { + output = formatTree(graph, symbol); } - await gracefulExit(); + + console.log(output); + } finally { + await db.close(); } + + await gracefulExit(); }); diff --git a/src/commands/doctor.ts b/src/commands/utility/doctor.ts similarity index 57% rename from src/commands/doctor.ts rename to src/commands/utility/doctor.ts index 58874f7d..0719f14c 100644 --- a/src/commands/doctor.ts +++ b/src/commands/utility/doctor.ts @@ -2,9 +2,10 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { Command } from "commander"; -import { MODEL_IDS, PATHS } from "../config"; -import { gracefulExit } from "../lib/utils/exit"; -import { findProjectRoot } from "../lib/utils/project-root"; +import { PATHS } from "../../config"; +import { initNative } from "../../lib/native"; +import { gracefulExit } from "../../lib/utils/exit"; +import { findProjectRoot } from "../../lib/utils/project-root"; export const doctor = new Command("doctor") .description("Check osgrep health and paths") @@ -12,9 +13,7 @@ export const doctor = new Command("doctor") console.log("šŸ„ osgrep Doctor\n"); const root = PATHS.globalRoot; - const models = PATHS.models; const grammars = PATHS.grammars; - const modelIds = [MODEL_IDS.embed, MODEL_IDS.colbert]; const checkDir = (name: string, p: string) => { const exists = fs.existsSync(p); @@ -23,24 +22,15 @@ export const doctor = new Command("doctor") }; checkDir("Root", root); - checkDir("Models", models); checkDir("Grammars", grammars); - const modelStatuses = modelIds.map((id) => { - const modelPath = path.join(models, ...id.split("/")); - return { id, path: modelPath, exists: fs.existsSync(modelPath) }; - }); - - modelStatuses.forEach(({ id, path: p, exists }) => { - const symbol = exists ? "āœ…" : "āŒ"; - console.log(`${symbol} Model: ${id} (${p})`); - }); - - const missingModels = modelStatuses.filter(({ exists }) => !exists); - if (missingModels.length > 0) { - console.log( - "āŒ Some models are missing; osgrep will try bundled copies first, then download.", - ); + try { + await initNative(); + console.log("āœ… Native models initialized"); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.log(`āŒ Native init failed: ${msg}`); + console.log(" Try: osgrep setup"); } console.log(`\nLocal Project: ${process.cwd()}`); diff --git a/src/commands/list.ts b/src/commands/utility/list.ts similarity index 94% rename from src/commands/list.ts rename to src/commands/utility/list.ts index 67a84189..a39b44d0 100644 --- a/src/commands/list.ts +++ b/src/commands/utility/list.ts @@ -1,8 +1,8 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { Command } from "commander"; -import { gracefulExit } from "../lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "../lib/utils/project-root"; +import { gracefulExit } from "../../lib/utils/exit"; +import { ensureProjectPaths, findProjectRoot } from "../../lib/utils/project-root"; const style = { bold: (s: string) => `\x1b[1m${s}\x1b[22m`, diff --git a/src/commands/verify.ts b/src/commands/verify.ts deleted file mode 100644 index 9484fa70..00000000 --- a/src/commands/verify.ts +++ /dev/null @@ -1,169 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { AutoTokenizer } from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; - -// CONFIGURATION -const MODEL_DIR = path.resolve("./osgrep-models/colbert"); // Adjust if your path differs -const MODEL_PATH = path.join(MODEL_DIR, "model.onnx"); -const SKIPLIST_PATH = path.join(MODEL_DIR, "skiplist.json"); - -async function main() { - console.log("šŸ” Starting ColBERT Integrity Check...\n"); - - // --- CHECK 1: FILES EXIST --- - if (!fs.existsSync(MODEL_PATH)) - throw new Error(`Missing model at ${MODEL_PATH}`); - if (!fs.existsSync(SKIPLIST_PATH)) - throw new Error(`Missing skiplist at ${SKIPLIST_PATH}`); - console.log("āœ… Files found."); - - // --- CHECK 2: TOKENIZER & MARKERS --- - console.log("ā³ Loading Tokenizer..."); - const tokenizer = await AutoTokenizer.from_pretrained(MODEL_DIR); - - const queryText = "function test(a, b)"; - // We manually add the [Q] marker to simulate what the worker does - // Note: We use the ID we know works from your export: 50368 - // But let's see if the tokenizer resolves "[Q] " correctly. - - const encoded = await tokenizer(queryText, { add_special_tokens: false }); - const inputIds = encoded.input_ids; // BigInt64Array in newer transformers versions - - // Convert to standard array for inspection - const ids = Array.from(inputIds).map(Number); - - // Mixedbread expects: [CLS] [Q] ...tokens... [SEP] - // Let's verify we can construct that. - const Q_ID = 50368; - const CLS_ID = tokenizer.model.tokens_to_ids.get("[CLS]") ?? 50281; // Fallback to standard if null - - console.log(`\n--- Tokenizer Check ---`); - console.log(`Query: "${queryText}"`); - console.log(`Raw IDs:`, ids); - - // Check if tokenizer recognizes the special tokens by text - const qCheck = tokenizer.model.tokens_to_ids.get("[Q] "); - const dCheck = tokenizer.model.tokens_to_ids.get("[D] "); - - if (qCheck === 50368 && dCheck === 50369) { - console.log(`āœ… Tokenizer Map Correct: [Q] -> ${qCheck}, [D] -> ${dCheck}`); - } else { - console.error( - `āŒ Tokenizer Map Mismatch! Found [Q]->${qCheck}, [D]->${dCheck}`, - ); - console.error(` Expected 50368 and 50369.`); - } - - // --- CHECK 3: SKIPLIST --- - const skiplist = new Set(JSON.parse(fs.readFileSync(SKIPLIST_PATH, "utf-8"))); - console.log(`\n--- Skiplist Check ---`); - console.log(`Skiplist size: ${skiplist.size}`); - - // Check common punctuation - const commaId = tokenizer.model.tokens_to_ids.get(","); - const dotId = tokenizer.model.tokens_to_ids.get("."); - - if (skiplist.has(commaId) && skiplist.has(dotId)) { - console.log( - `āœ… Skiplist contains punctuation ('.'=${dotId}, ','=${commaId})`, - ); - } else { - console.error(`āŒ Skiplist missing basic punctuation!`); - } - - // --- CHECK 4: ONNX INFERENCE --- - console.log(`\n--- ONNX Inference Check ---`); - const session = await ort.InferenceSession.create(MODEL_PATH); - console.log(`Session loaded. Input names: ${session.inputNames}`); - - // Construct a dummy batch: [CLS] [Q] test [SEP] - const batchIds = [ - BigInt(CLS_ID), - BigInt(Q_ID), - BigInt(1234), - BigInt(tokenizer.sep_token_id ?? 50282), - ]; - const tensorIds = new ort.Tensor( - "int64", - new BigInt64Array(batchIds), - [1, 4], - ); - const tensorMask = new ort.Tensor( - "int64", - new BigInt64Array([BigInt(1), BigInt(1), BigInt(1), BigInt(1)]), - [1, 4], - ); - - const start = performance.now(); - const feeds = { input_ids: tensorIds, attention_mask: tensorMask }; - const results = await session.run(feeds); - const end = performance.now(); - - const outputName = session.outputNames[0]; - const embeddings = results[outputName]; - - // Dims should be [1, 4, 48] - const dims = embeddings.dims; - console.log(`Output Dimensions: [${dims.join(", ")}]`); - console.log(`Inference Time (cold): ${(end - start).toFixed(2)}ms`); - - if (dims[2] !== 48) { - console.error(`āŒ CRITICAL: Expected dimension 48, got ${dims[2]}`); - process.exit(1); - } else { - console.log(`āœ… Correct dimension (48d) detected.`); - } - - // --- CHECK 5: MAXSIM PERFORMANCE SIMULATION --- - console.log(`\n--- MaxSim Logic Benchmark ---`); - - // Create dummy vectors for a fake document (1000 tokens) - const docLen = 1000; - const docIds = new Array(docLen) - .fill(0) - .map(() => Math.floor(Math.random() * 50000)); - - // Inject some punctuation into the dummy document to simulate real text - // Let's say 15% of the doc is punctuation - let punctuationCount = 0; - for (let i = 0; i < docLen; i++) { - if (Math.random() < 0.15) { - docIds[i] = commaId ?? 0; // Force a comma - punctuationCount++; - } - } - - const qLen = 32; - - // Naive Dot Product count - const naiveOps = qLen * docLen; - - // Skiplist Dot Product count - let optimizedOps = 0; - for (let i = 0; i < qLen; i++) { - for (let j = 0; j < docLen; j++) { - if (!skiplist.has(docIds[j])) { - optimizedOps++; - } - } - } - - console.log(`Document Length: ${docLen} tokens`); - console.log( - `Punctuation/Skip tokens: ${punctuationCount} (~${((punctuationCount / docLen) * 100).toFixed(1)}%)`, - ); - console.log(`Naive Operations: ${naiveOps}`); - console.log(`Skiplist Operations: ${optimizedOps}`); - console.log(`Savings: ${naiveOps - optimizedOps} operations avoided`); - console.log( - `⚔ Speedup: ${(naiveOps / optimizedOps).toFixed(2)}x (theoretical)`, - ); - - console.log("\nāœ… VERIFICATION COMPLETE. MODEL IS GOOD TO GO."); -} - -main().catch((err) => { - console.error("\nāŒ TEST FAILED:", err); - process.exit(1); -}); diff --git a/src/config.ts b/src/config.ts index 65518218..511cb697 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,33 +25,11 @@ export const CONFIG = { QUERY_PREFIX: "", }; -export const WORKER_TIMEOUT_MS = Number.parseInt( - process.env.OSGREP_WORKER_TIMEOUT_MS || "60000", - 10, -); - -export const WORKER_BOOT_TIMEOUT_MS = Number.parseInt( - process.env.OSGREP_WORKER_BOOT_TIMEOUT_MS || "300000", - 10, -); - -export const MAX_WORKER_MEMORY_MB = Number.parseInt( - process.env.OSGREP_MAX_WORKER_MEMORY_MB || - String( - Math.max( - 2048, - Math.floor((os.totalmem() / 1024 / 1024) * 0.5), // 50% of system RAM - ), - ), - 10, -); - const HOME = os.homedir(); const GLOBAL_ROOT = path.join(HOME, ".osgrep"); export const PATHS = { globalRoot: GLOBAL_ROOT, - models: path.join(GLOBAL_ROOT, "models"), grammars: path.join(GLOBAL_ROOT, "grammars"), }; diff --git a/src/eval.ts b/src/eval.ts deleted file mode 100644 index 82a3d875..00000000 --- a/src/eval.ts +++ /dev/null @@ -1,673 +0,0 @@ -// Reduce worker pool fan-out during eval to avoid ONNX concurrency issues -process.env.OSGREP_WORKER_COUNT ??= "1"; - -import { Searcher } from "./lib/search/searcher"; -import type { SearchResponse } from "./lib/store/types"; -import { VectorDB } from "./lib/store/vector-db"; -import { gracefulExit } from "./lib/utils/exit"; -import { ensureProjectPaths, findProjectRoot } from "./lib/utils/project-root"; - -export type EvalCase = { - query: string; - expectedPath: string; - avoidPath?: string; // <--- New field: If this ranks HIGHER than expected, it's a fail. - note?: string; -}; - -export type EvalResult = { - rr: number; - found: boolean; - recall: number; - path: string; - query: string; - note?: string; - timeMs: number; -}; - -export const cases: EvalCase[] = [ - // --- Search & Ranking --- - { - query: "How do we merge vector and keyword results before rerank?", - expectedPath: "src/lib/search/searcher.ts", - note: "Hybrid search path that stitches LanceDB vector search with FTS.", - }, - { - query: "Where do we dedupe overlapping vector and FTS candidates?", - expectedPath: "src/lib/search/searcher.ts", - note: "Combines results and removes duplicates ahead of rerank.", - }, - { - query: "How do we boost functions and downweight tests or docs?", - expectedPath: "src/lib/search/searcher.ts", - note: "applyStructureBoost handles path/type based adjustments.", - }, - { - query: "What controls the pre-rerank candidate fanout?", - expectedPath: "src/lib/search/searcher.ts", - note: "PRE_RERANK_K calculation before ColBERT scoring.", - }, - { - query: "How do we filter searches to a path prefix?", - expectedPath: "src/lib/search/searcher.ts", - note: "Path prefix WHERE clause for scoped queries.", - }, - { - query: "How do we apply ColBERT rerank scoring to candidates?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "Worker-side rerank that feeds query/docs into maxSim.", - }, - { - query: "ColBERT maxSim scoring implementation", - expectedPath: "src/lib/workers/colbert-math.ts", - note: "Summed max dot products between query and doc token grids.", - }, - - // --- Worker Pool & Embeddings --- - { - query: "Why are ONNX workers child processes instead of threads?", - expectedPath: "src/lib/workers/pool.ts", - note: "Process pool choice to isolate runtime crashes.", - }, - { - query: "How do we timeout and restart stuck worker tasks?", - expectedPath: "src/lib/workers/pool.ts", - note: "Task timeout handling that kills and respawns workers.", - }, - { - query: "Which script does the worker pool fork at runtime?", - expectedPath: "src/lib/workers/pool.ts", - note: "resolveProcessWorker chooses process-child entrypoint.", - }, - { - query: "How does worker pool shutdown terminate children?", - expectedPath: "src/lib/workers/pool.ts", - note: "destroy() kills processes with SIGTERM/SIGKILL fallback.", - }, - { - query: "Where are Granite embeddings loaded from onnx cache?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "resolvePaths + load selecting ONNX weights and tokenizer.", - }, - { - query: "How do we mean-pool Granite outputs to 384 dimensions?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "meanPool normalizes and pads vectors to CONFIG.VECTOR_DIM.", - }, - { - query: "How does ColBERT quantize token grids to int8 with a scale?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "runBatch builds int8 arrays and records maxVal scale.", - }, - { - query: "Where do we compute pooled_colbert_48d summaries?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "Per-chunk pooled embedding stored alongside dense vectors.", - }, - { - query: "How do we normalize ColBERT query embeddings before rerank?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "encodeQuery builds normalized matrix from ONNX output.", - }, - { - query: "How are dense and ColBERT embeddings combined for each chunk?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "computeHybrid pairs Granite dense vectors with ColBERT grids.", - }, - { - query: "Where do we build anchor chunks with imports and preamble?", - expectedPath: "src/lib/index/chunker.ts", - note: "buildAnchorChunk prepends metadata-heavy anchor blocks.", - }, - { - query: "How is breadcrumb formatting added to chunk text?", - expectedPath: "src/lib/index/chunker.ts", - note: "formatChunkText injects file + context headers.", - }, - { - query: "What are the chunk overlap and max size settings?", - expectedPath: "src/lib/index/chunker.ts", - note: "MAX_CHUNK_LINES/CHARS and OVERLAP tuning in TreeSitterChunker.", - }, - { - query: "How do we fall back when a Tree-sitter grammar is missing?", - expectedPath: "src/lib/index/chunker.ts", - note: "chunk() fallback path when parser/grammar cannot load.", - }, - { - query: "Where are grammars downloaded and cached?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "GRAMMARS_DIR and ensureGrammars downloader.", - }, - { - query: "Which languages and grammars are supported for chunking?", - expectedPath: "src/lib/core/languages.ts", - note: "LANGUAGES table that maps extensions to grammars.", - }, - - // --- Indexing & Sync --- - { - query: "Where do we enforce a writer lock to prevent concurrent indexing?", - expectedPath: "src/lib/utils/lock.ts", - note: "LOCK file acquisition and stale process detection.", - }, - { - query: "Where is DEFAULT_IGNORE_PATTERNS defined for indexing?", - expectedPath: "src/lib/index/ignore-patterns.ts", - note: "DEFAULT_IGNORE_PATTERNS with lockfiles and secrets.", - }, - { - query: "Which INDEXABLE_EXTENSIONS are allowed and what is the 10MB limit?", - expectedPath: "src/config.ts", - note: "INDEXABLE_EXTENSIONS and MAX_FILE_SIZE_BYTES.", - }, - { - query: "How do we reset when VectorDB and meta cache disagree?", - expectedPath: "src/lib/index/syncer.ts", - note: "Inconsistency detection that forces drop + rebuild.", - }, - { - query: "How are batches flushed to LanceDB before updating meta cache?", - expectedPath: "src/lib/index/syncer.ts", - note: "flushBatch writes VectorDB first, then meta entries.", - }, - { - query: "How do we remove stale or deleted paths from the index?", - expectedPath: "src/lib/index/syncer.ts", - note: "Cleanup of stale paths after scanning is finished.", - }, - { - query: "How do we skip unchanged files using mtime/size hashes?", - expectedPath: "src/lib/index/syncer.ts", - note: "Meta cache check to bypass re-embedding identical files.", - }, - { - query: - "When does processFile mark shouldDelete for binary, empty, or too-big files?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "processFile returns shouldDelete for non-indexable snapshots.", - }, - { - query: "How is the file hash computed for change detection?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "computeBufferHash SHA-256 helper.", - }, - { - query: "How do we snapshot a file and verify it didn't change during read?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "readFileSnapshot double-checks size/mtime before returning.", - }, - - // --- Storage & Schema --- - { - query: "Where is the LanceDB schema defined, including pooled_colbert_48d?", - expectedPath: "src/lib/store/vector-db.ts", - note: "Schema with dense, colbert, and pooled embeddings.", - }, - { - query: "How do we warn about schema mismatches and ask for a reindex?", - expectedPath: "src/lib/store/vector-db.ts", - note: "insertBatch error message for field mismatches.", - }, - { - query: "Where do we create the full-text index on chunk content?", - expectedPath: "src/lib/store/vector-db.ts", - note: "createFTSIndex invoking LanceDB FTS.", - }, - - // --- CLI Commands --- - { - query: - "How does the search command trigger initial indexing when the store is empty?", - expectedPath: "src/commands/search.ts", - note: "Checks hasAnyRows and runs initialSync + spinner.", - }, - { - query: "Where does search --dry-run print formatDryRunSummary?", - expectedPath: "src/commands/search.ts", - note: "formatDryRunSummary usage for dry-run summaries.", - }, - { - query: - "How does the index command handle --reset then call createFTSIndex?", - expectedPath: "src/commands/index.ts", - note: "Indexing workflow before createFTSIndex.", - }, - { - query: "How does serve reject search paths outside the project root?", - expectedPath: "src/commands/serve.ts", - note: "Path normalization rejecting traversal outside projectRoot.", - }, - { - query: "Where does the server enforce a 1MB payload size limit?", - expectedPath: "src/commands/serve.ts", - note: "Request body guard that 413s payloads over 1MB.", - }, - { - query: - "How does serve --background redirect logs to ~/.osgrep/logs/server.log?", - expectedPath: "src/commands/serve.ts", - note: "Background flag redirecting stdio to server.log.", - }, - { - query: "Where does setup ensureSetup runs and grammars get downloaded?", - expectedPath: "src/commands/setup.ts", - note: "Setup command invoking ensureSetup and ensureGrammars.", - }, - { - query: "How does doctor check PATHS.models for missing model directories?", - expectedPath: "src/commands/doctor.ts", - note: "Health checks for PATHS.models and MODEL_IDS.", - }, - { - query: "Where is Claude Code plugin installation defined?", - expectedPath: "src/commands/claude-code.ts", - note: "Marketplace add + install flow.", - }, - - // --- Paths, Config, Environment --- - { - query: "How do we create .osgrep directories and add them to .gitignore?", - expectedPath: "src/lib/utils/project-root.ts", - note: "ensureProjectPaths scaffolds directories and gitignore entry.", - }, - { - query: "How is the project root detected via .git or existing .osgrep?", - expectedPath: "src/lib/utils/project-root.ts", - note: "findProjectRoot walking parents and honoring repo roots.", - }, - { - query: "Where are PATHS.globalRoot, models, and grammars defined?", - expectedPath: "src/config.ts", - note: "PATHS pointing to ~/.osgrep directories.", - }, - { - query: "How do workers prefer a local ./models directory when present?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "env.localModelPath override when repo ships models.", - }, - { - query: "Where are VECTOR_DIM, COLBERT_DIM, and WORKER_THREADS configured?", - expectedPath: "src/config.ts", - note: "CONFIG with VECTOR_DIM, COLBERT_DIM, WORKER_THREADS.", - }, - - // --- Extended Coverage --- - { - query: "Where do we read WORKER_TIMEOUT_MS from OSGREP_WORKER_TIMEOUT_MS?", - expectedPath: "src/config.ts", - note: "WORKER_TIMEOUT_MS env override.", - }, - { - query: "Where is TASK_TIMEOUT_MS set for worker tasks?", - expectedPath: "src/lib/workers/pool.ts", - note: "OSGREP_WORKER_TASK_TIMEOUT_MS guarded timeout.", - }, - { - query: - "How do we cap worker threads from OSGREP_WORKER_THREADS with a HARD_CAP of 4?", - expectedPath: "src/config.ts", - note: "DEFAULT_WORKER_THREADS calculation.", - }, - { - query: "Where do we set HF transformers cacheDir and allowLocalModels?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "env.cacheDir and env.allowLocalModels toggles.", - }, - { - query: "Where do we load Granite ONNX with CPU execution providers?", - expectedPath: "src/lib/workers/embeddings/granite.ts", - note: "load() builds sessionOptions for cpu backend.", - }, - { - query: "Where do we limit ColBERT ONNX runtime threads to 1?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "ONNX_THREADS constant and session options.", - }, - { - query: - "How do we normalize ColBERT doc vectors and quantize to int8 scale?", - expectedPath: "src/lib/workers/embeddings/colbert.ts", - note: "runBatch builds normalized grid and scale factor.", - }, - { - query: "Where do we normalize ColBERT query rows before building matrix?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "encodeQuery normalizes ONNX output rows.", - }, - { - query: - "Where do we convert serialized Buffer objects to Int8Array for rerank?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "rerank converts Buffer/object into Int8Array.", - }, - { - query: "Where do we build UUIDs for chunk ids before inserting to LanceDB?", - expectedPath: "src/lib/workers/orchestrator.ts", - note: "toPreparedChunks uses uuidv4 for chunk IDs.", - }, - { - query: - "How do we include imports, exports, and top comments in anchor chunks?", - expectedPath: "src/lib/index/chunker.ts", - note: "buildAnchorChunk composes sections with metadata.", - }, - { - query: "Where do we warn about missing tree-sitter grammars and fall back?", - expectedPath: "src/lib/index/chunker.ts", - note: "chunk() logs and falls back when getLanguage fails.", - }, - { - query: "Where do we split oversized chunks with line and char overlaps?", - expectedPath: "src/lib/index/chunker.ts", - note: "splitIfTooBig uses OVERLAP_LINES and OVERLAP_CHARS.", - }, - { - query: "Where is GRAMMARS_DIR set to ~/.osgrep/grammars?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "GRAMMARS_DIR constant.", - }, - { - query: "Where do we download grammars with fetch and a custom User-Agent?", - expectedPath: "src/lib/index/grammar-loader.ts", - note: "ensureGrammars downloadFile helper.", - }, - { - query: "Where do we guard against files changing during read?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "readFileSnapshot compares pre/post stats.", - }, - { - query: "Where do we detect null bytes before indexing content?", - expectedPath: "src/lib/utils/file-utils.ts", - note: "hasNullByte check in processFile path.", - }, - { - query: "Where do we register cleanup tasks and execute them at exit?", - expectedPath: "src/lib/utils/cleanup.ts", - note: "registerCleanup and runCleanup functions.", - }, - { - query: "Where does gracefulExit destroy the worker pool before exiting?", - expectedPath: "src/lib/utils/exit.ts", - note: "gracefulExit calls destroyWorkerPool and runCleanup.", - }, - { - query: "Where is the LMDB meta cache opened with compression?", - expectedPath: "src/lib/store/meta-cache.ts", - note: "MetaCache constructor uses lmdb open() with compression.", - }, - { - query: "Where do we connect to LanceDB and seed the table schema?", - expectedPath: "src/lib/store/vector-db.ts", - note: "ensureTable creates schema and deletes seed row.", - }, - { - query: "Where do we drop the LanceDB table during resets?", - expectedPath: "src/lib/store/vector-db.ts", - note: "drop() helper invoked on reset.", - }, - { - query: "Where do we close LanceDB connections and unregister cleanup?", - expectedPath: "src/lib/store/vector-db.ts", - note: "close() method clears connections and cleanup hook.", - }, - { - query: "Where do we use fast-glob to stream files for indexing?", - expectedPath: "src/lib/index/syncer.ts", - note: "fg.stream with glob options for repo walk.", - }, - { - query: "Where do we skip duplicate real paths and broken symlinks?", - expectedPath: "src/lib/index/syncer.ts", - note: "visitedRealPaths plus try/catch around realpathSync.", - }, - { - query: "Where do we abort indexing when AbortSignal is triggered?", - expectedPath: "src/lib/index/syncer.ts", - note: "Checks signal.aborted to stop scheduling.", - }, - { - query: - "Where do we flush batches when batch/deletes/meta reach batchLimit?", - expectedPath: "src/lib/index/syncer.ts", - note: "flush() checks batchLimit based on EMBED_BATCH_SIZE.", - }, - { - query: - "Where do we detect stale cached paths and delete them after indexing?", - expectedPath: "src/lib/index/syncer.ts", - note: "Removes stale paths from VectorDB and meta cache.", - }, - { - query: - "Where do we detect inconsistent VectorDB vs meta cache and force rebuild?", - expectedPath: "src/lib/index/syncer.ts", - note: "isInconsistent triggers drop and meta reset.", - }, - { - query: - "Where is createIndexingSpinner updating text for scanning and indexing files?", - expectedPath: "src/lib/index/sync-helpers.ts", - note: "createIndexingSpinner onProgress formatting.", - }, - { - query: - "Where does ensureSetup create ~/.osgrep directories with ora spinner?", - expectedPath: "src/lib/setup/setup-helpers.ts", - note: "ensureSetup directory creation feedback.", - }, - { - query: - "Where do we download models via a worker thread to avoid ONNX in main thread?", - expectedPath: "src/lib/setup/model-loader.ts", - note: "downloadModels spawns worker with ts-node/register when dev.", - }, - { - query: "Where do we check areModelsDownloaded before running setup?", - expectedPath: "src/lib/setup/model-loader.ts", - note: "areModelsDownloaded verifies cache directories.", - }, - { - query: - "Where does setup command list model and grammar status after finishing?", - expectedPath: "src/commands/setup.ts", - note: "Setup command status output with model IDs.", - }, - { - query: "Where does doctor print system platform, arch, and Node version?", - expectedPath: "src/commands/doctor.ts", - note: "Doctor command system info logging.", - }, - { - query: "Where does the list command calculate directory sizes recursively?", - expectedPath: "src/commands/list.ts", - note: "getDirectorySize walk.", - }, - { - query: "Where does the list command format sizes and time ago text?", - expectedPath: "src/commands/list.ts", - note: "formatSize and formatDate helpers.", - }, - { - query: "Where does serve register running servers to servers.json?", - expectedPath: "src/lib/utils/server-registry.ts", - note: "registerServer writes to ~/.osgrep/servers.json.", - }, - { - query: "How does serve status enumerate active servers?", - expectedPath: "src/commands/serve.ts", - note: "serve status subcommand uses listServers().", - }, - { - query: "How does serve stop --all kill background servers?", - expectedPath: "src/commands/serve.ts", - note: "serve stop iterates listServers and SIGTERMs.", - }, - { - query: "Where is the LOCK file written and stale PID detection handled?", - expectedPath: "src/lib/utils/lock.ts", - note: "acquireWriterLock parses existing lock with pid/start time.", - }, - { - query: "Where do we parse .git worktree files to find the main repo root?", - expectedPath: "src/lib/utils/git.ts", - note: "getMainRepoRoot and getGitCommonDir for worktrees.", - }, - { - query: "Where do we format search results in plain mode for agents?", - expectedPath: "src/lib/utils/formatter.ts", - note: "formatTextResults plain mode with agent tags.", - }, - { - query: "Where do we apply syntax highlighting for human output?", - expectedPath: "src/lib/utils/formatter.ts", - note: "formatTextResults uses cli-highlight when not plain.", - }, - { - query: - "Where do we merge nearby snippets from the same file before printing?", - expectedPath: "src/lib/utils/formatter.ts", - note: "Smart stitching merges overlapping chunks per file.", - }, - { - query: - "Where are search CLI options like --scores, --compact, --per-file handled?", - expectedPath: "src/commands/search.ts", - note: "Commander options declared for search command.", - }, - { - query: "Where is the search path argument normalized against project root?", - expectedPath: "src/commands/search.ts", - note: "Relative path handling before searcher.search.", - }, -]; - -const topK = 20; - -export function evaluateCase( - response: SearchResponse, - evalCase: EvalCase, - timeMs: number, -): EvalResult { - const expectedPaths = evalCase.expectedPath - .split("|") - .map((p) => p.trim().toLowerCase()) - .filter(Boolean); - - const rank = response.data.findIndex((chunk) => { - const path = chunk.metadata?.path?.toLowerCase() || ""; - return expectedPaths.some((expected) => path.includes(expected)); - }); - - const avoidRank = response.data.findIndex((chunk) => - chunk.metadata?.path - ?.toLowerCase() - .includes(evalCase.avoidPath?.toLowerCase() || "_____"), - ); - - const hitAvoid = - evalCase.avoidPath && avoidRank >= 0 && (rank === -1 || avoidRank < rank); - const found = rank >= 0 && !hitAvoid; - const rr = found ? 1 / (rank + 1) : 0; - const recall = found && rank < 10 ? 1 : 0; - - return { - rr, - found, - recall, - path: evalCase.expectedPath, - query: evalCase.query, - note: evalCase.note, - timeMs, - }; -} - -async function run() { - const root = process.cwd(); - const searchRoot = root; - const projectRoot = findProjectRoot(searchRoot) ?? searchRoot; - const paths = ensureProjectPaths(projectRoot); - const vectorDb = new VectorDB(paths.lancedbDir); - const searcher = new Searcher(vectorDb); - - // 1. Ensure the store exists (VectorDB handles creation, but we check for data) - const hasRows = await vectorDb.hasAnyRows(); - if (!hasRows) { - console.error(`āŒ Store appears to be empty!`); - console.error(` Run "osgrep index" to populate the store with data.`); - process.exit(1); - } - - // 2. Check if store has data (redundant but good for sanity) - try { - const testResult = await searcher.search("test", 1, { rerank: true }); - if (testResult.data.length === 0) { - console.error( - `āš ļø Store appears to be empty (search returned 0 results)!`, - ); - console.error(` Run "osgrep index" to populate the store with data.`); - process.exit(1); - } - } catch (err) { - console.error(`āŒ Error checking store data:`, err); - process.exit(1); - } - - const results: EvalResult[] = []; - - console.log("Starting evaluation...\n"); - const startTime = performance.now(); - - for (const c of cases) { - const queryStart = performance.now(); - const res = await searcher.search(c.query, topK, { rerank: false }); - const queryEnd = performance.now(); - const timeMs = queryEnd - queryStart; - - results.push(evaluateCase(res, c, timeMs)); - } - - const totalTime = performance.now() - startTime; - const mrr = results.reduce((sum, r) => sum + r.rr, 0) / results.length; - const recallAt10 = - results.reduce((sum, r) => sum + r.recall, 0) / results.length; - const avgTime = - results.reduce((sum, r) => sum + r.timeMs, 0) / results.length; - - console.log("=".repeat(80)); - console.log(`Eval results for store at: ${paths.lancedbDir}`); - console.log("=".repeat(80)); - results.forEach((r) => { - const status = r.found ? `rank ${(1 / r.rr).toFixed(0)}` : "āŒ missed"; - const emoji = r.found ? (r.rr === 1 ? "šŸŽÆ" : "āœ“") : "āŒ"; - console.log(`${emoji} ${r.query}`); - console.log( - ` => ${status} (target: ${r.path}) [${r.timeMs.toFixed(0)}ms]`, - ); - if (r.note) { - console.log(` // ${r.note}`); - } - }); - console.log("=".repeat(80)); - console.log(`MRR: ${mrr.toFixed(3)}`); - console.log(`Recall@10: ${recallAt10.toFixed(3)}`); - console.log(`Avg query time: ${avgTime.toFixed(0)}ms`); - console.log(`Total time: ${totalTime.toFixed(0)}ms`); - console.log( - `Found: ${results.filter((r) => r.found).length}/${results.length}`, - ); - console.log("=".repeat(80)); - - await gracefulExit(0); -} - -if ( - // Only auto-run when executed directly (not when imported for experiments/tests) - require.main === module && - process.env.OSGREP_EVAL_AUTORUN !== "0" -) { - run().catch((err) => { - console.error("Eval failed:", err); - gracefulExit(1); - }); -} diff --git a/src/index.ts b/src/index.ts index 8387883c..5a1d770a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,18 +2,17 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { program } from "commander"; -import { installClaudeCode } from "./commands/claude-code"; -import { installCodex } from "./commands/codex"; -import { doctor } from "./commands/doctor"; -import { installDroid } from "./commands/droid"; +import { installClaudeCode } from "./commands/agent/claude-code"; +import { installCodex } from "./commands/agent/codex"; +import { doctor } from "./commands/utility/doctor"; +import { installDroid } from "./commands/agent/droid"; import { index } from "./commands/index"; -import { list } from "./commands/list"; -import { mcp } from "./commands/mcp"; -import { installOpencode, uninstallOpencode } from "./commands/opencode"; +import { list } from "./commands/utility/list"; +import { mcp } from "./commands/agent/mcp"; +import { installOpencode, uninstallOpencode } from "./commands/agent/opencode"; import { search } from "./commands/search"; import { serve } from "./commands/serve"; import { setup } from "./commands/setup"; -import { skeleton } from "./commands/skeleton"; import { symbols } from "./commands/symbols"; import { trace } from "./commands/trace"; @@ -48,7 +47,6 @@ if (isIndexCommand && fs.existsSync(legacyDataPath)) { program.addCommand(search, { isDefault: true }); program.addCommand(index); program.addCommand(list); -program.addCommand(skeleton); program.addCommand(symbols); program.addCommand(trace); program.addCommand(setup); diff --git a/src/lib/graph/graph-builder.ts b/src/lib/graph/graph-builder.ts index a715feb2..9c04ec49 100644 --- a/src/lib/graph/graph-builder.ts +++ b/src/lib/graph/graph-builder.ts @@ -1,6 +1,6 @@ -import type { VectorRecord } from "../store/types"; import type { VectorDB } from "../store/vector-db"; -import { escapeSqlString } from "../utils/filter-builder"; +import type { VectorRecord } from "../store/types"; +import { escapeSqlString, normalizePath } from "../utils/filter-builder"; export interface GraphNode { symbol: string; @@ -8,122 +8,166 @@ export interface GraphNode { line: number; role: string; calls: string[]; - calledBy: string[]; - complexity?: number; +} + +export interface CallGraph { + center: GraphNode | null; + callers: GraphNode[]; + callees: string[]; +} + +export interface TraceOptions { + depth?: number; + callersOnly?: boolean; + calleesOnly?: boolean; + pathPrefix?: string; } export class GraphBuilder { constructor(private db: VectorDB) {} /** - * Find all chunks that call the given symbol. + * Find all chunks where the symbol is defined. + * Returns multiple if the same symbol is defined in different files. */ - async getCallers(symbol: string): Promise { + async findDefinitions(symbol: string): Promise { const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); + const whereClause = `array_contains(defined_symbols, '${escapeSqlString(symbol)}')`; - // Find chunks where referenced_symbols contains the symbol - const rows = await table + const records = (await table .query() - .where(`array_contains(referenced_symbols, '${escaped}')`) - .limit(100) - .toArray(); + .where(whereClause) + .limit(20) + .toArray()) as VectorRecord[]; - return rows.map((row) => - this.mapRowToNode(row as unknown as VectorRecord, symbol, "caller"), - ); + return records.map((r) => this.recordToNode(r, symbol)); } /** - * Find what the given symbol calls. - * First finds the definition of the symbol, then returns its referenced_symbols. + * Find chunks that reference (call) the given symbol. + * Excludes chunks that also define the symbol (to avoid self-references). */ - async getCallees(symbol: string): Promise { + async findCallers(symbol: string, limit = 20): Promise { const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); + const whereClause = `array_contains(referenced_symbols, '${escapeSqlString(symbol)}')`; - // Find the definition of the symbol - const rows = await table + const records = (await table .query() - .where(`array_contains(defined_symbols, '${escaped}')`) - .limit(1) - .toArray(); - - if (rows.length === 0) return []; - - const record = rows[0] as unknown as VectorRecord; - return record.referenced_symbols || []; + .where(whereClause) + .limit(limit + 20) // Fetch extra to filter self-definitions + .toArray()) as VectorRecord[]; + + // Filter out self-definitions (where this chunk also defines the symbol) + const filtered = records.filter((r) => { + const defined = this.toStringArray(r.defined_symbols); + return !defined.includes(symbol); + }); + + return filtered.slice(0, limit).map((r) => { + // Find the primary symbol for this chunk (caller) + const defined = this.toStringArray(r.defined_symbols); + const callerSymbol = defined[0] || r.parent_symbol || "(anonymous)"; + return this.recordToNode(r, callerSymbol); + }); } /** - * Build a 1-hop graph around a symbol. + * Given a list of symbol names, return only those that have definitions + * in the index (i.e., internal to the project, not external libraries). */ - async buildGraph(symbol: string): Promise<{ - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; - }> { - const table = await this.db.ensureTable(); - const escaped = escapeSqlString(symbol); - - // 1. Get Center (Definition) - const centerRows = await table - .query() - .where(`array_contains(defined_symbols, '${escaped}')`) - .limit(1) - .toArray(); - - const center = - centerRows.length > 0 - ? this.mapRowToNode( - centerRows[0] as unknown as VectorRecord, - symbol, - "center", - ) - : null; + async filterToInternal(symbols: string[]): Promise { + if (symbols.length === 0) return []; - // 2. Get Callers - const callers = await this.getCallers(symbol); - - // 3. Get Callees (from center) - const callees = center ? center.calls : []; + const table = await this.db.ensureTable(); + const internal: string[] = []; + + // Batch check which symbols have definitions + // For efficiency, we use a single query with OR conditions + // But LanceDB doesn't support OR in array_contains well, so we check individually + // This is acceptable since callees are typically <20 per function + for (const sym of symbols) { + const whereClause = `array_contains(defined_symbols, '${escapeSqlString(sym)}')`; + const records = await table.query().where(whereClause).limit(1).toArray(); + if (records.length > 0) { + internal.push(sym); + } + } - return { center, callers, callees }; + return internal; } - private mapRowToNode( - row: VectorRecord, - targetSymbol: string, - type: "center" | "caller", - ): GraphNode { - // Helper to convert Arrow Vector to array if needed - const toArray = (val: any): string[] => { - if (val && typeof val.toArray === "function") { - return val.toArray(); - } - return Array.isArray(val) ? val : []; - }; + /** + * Build the call graph for a symbol. + * Returns the definition (center), callers, and callees (filtered to internal). + */ + async buildGraph( + symbol: string, + options?: TraceOptions, + ): Promise { + const { callersOnly, calleesOnly, pathPrefix } = options || {}; + + // Find definitions + let definitions = await this.findDefinitions(symbol); + + // Apply path prefix filter if specified + if (pathPrefix) { + const normalizedPrefix = normalizePath(pathPrefix); + definitions = definitions.filter((d) => + d.file.startsWith(normalizedPrefix), + ); + } - const definedSymbols = toArray(row.defined_symbols); - const referencedSymbols = toArray(row.referenced_symbols); + // For now, take the first definition as center (could show all) + const center = definitions[0] || null; - // If it's a caller, the symbol of interest is the one DOING the calling. - // We try to find the defined symbol in this chunk that is responsible for the call. - // If multiple are defined, we pick the first one or the parent_symbol. + // Get callers if not callees-only + let callers: GraphNode[] = []; + if (!calleesOnly) { + callers = await this.findCallers(symbol); + if (pathPrefix) { + const normalizedPrefix = normalizePath(pathPrefix); + callers = callers.filter((c) => c.file.startsWith(normalizedPrefix)); + } + } - let symbol = definedSymbols[0] || row.parent_symbol || "unknown"; - if (type === "center") { - symbol = targetSymbol; + // Get callees if not callers-only + let callees: string[] = []; + if (!callersOnly && center) { + // Get the raw referenced_symbols from the center definition + const rawCallees = center.calls; + // Filter to only internal symbols (have definitions in index) + callees = await this.filterToInternal(rawCallees); } + return { center, callers, callees }; + } + + private recordToNode(record: VectorRecord, symbol: string): GraphNode { return { symbol, - file: row.path, - line: row.start_line, - role: row.role || "IMPLEMENTATION", - calls: referencedSymbols, - calledBy: [], // To be filled if we do reverse lookup - complexity: row.complexity, + file: record.path, + line: record.start_line, + role: record.role || "IMPL", + calls: this.toStringArray(record.referenced_symbols), }; } + + private toStringArray(val: unknown): string[] { + if (!val) return []; + if (Array.isArray(val)) { + return val.filter((v) => typeof v === "string"); + } + if (typeof (val as any).toArray === "function") { + try { + const arr = (val as any).toArray(); + if (Array.isArray(arr)) return arr.filter((v) => typeof v === "string"); + return Array.from(arr || []).filter( + (v) => typeof v === "string", + ) as string[]; + } catch { + return []; + } + } + return []; + } } diff --git a/src/lib/index/chunker.ts b/src/lib/index/chunker.ts index 83251145..fa586a83 100644 --- a/src/lib/index/chunker.ts +++ b/src/lib/index/chunker.ts @@ -1,7 +1,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import { CONFIG } from "../../config"; -import { getLanguageByExtension } from "../core/languages"; +import { getLanguageByExtension } from "../store/languages"; import { GRAMMARS_DIR } from "./grammar-loader"; // web-tree-sitter ships a CommonJS build @@ -119,7 +119,7 @@ export function formatChunkText( const sections: string[] = []; // 1. File path (always first) - sections.push(`// ${filePath}`); + sections.push(`// File: ${filePath}`); // 2. Imports (if available) if (chunk.imports && chunk.imports.length > 0) { diff --git a/src/lib/index/grammar-loader.ts b/src/lib/index/grammar-loader.ts index 4c91fd50..6ef9feef 100644 --- a/src/lib/index/grammar-loader.ts +++ b/src/lib/index/grammar-loader.ts @@ -2,7 +2,7 @@ import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { LANGUAGES } from "../core/languages"; +import { LANGUAGES } from "../store/languages"; export const GRAMMARS_DIR = path.join(os.homedir(), ".osgrep", "grammars"); diff --git a/src/lib/index/ignore-patterns.ts b/src/lib/index/ignore-patterns.ts index f7245511..f6adb7a1 100644 --- a/src/lib/index/ignore-patterns.ts +++ b/src/lib/index/ignore-patterns.ts @@ -58,3 +58,41 @@ export const DEFAULT_IGNORE_PATTERNS = [ "**/.vscode/**", "Thumbs.db", ]; + +// Patterns for generated/auto-generated code files. +// These are still indexed but receive a score penalty in search results. +// Uses regex patterns (not globs) for matching against file paths. +export const GENERATED_FILE_PATTERNS: RegExp[] = [ + // TypeScript/JavaScript codegen + /\.gen\.[jt]sx?$/i, + /\.generated\.[jt]sx?$/i, + /\.g\.[jt]sx?$/i, + /_generated\.[jt]sx?$/i, + // TypeScript declaration files (ambient types, often auto-generated) + /\.d\.ts$/i, + // GraphQL codegen + /\.graphql\.[jt]sx?$/i, + /\/__generated__\//i, + // Protocol Buffers + /\.pb\.[a-z]+$/i, // .pb.go, .pb.ts, etc. + /_pb2\.py$/i, // Python protobuf + /_pb2_grpc\.py$/i, + // Go codegen + /_gen\.go$/i, + /_string\.go$/i, // stringer tool + /\.gen\.go$/i, + /mock_.*\.go$/i, // mockgen + // C# codegen + /\.Designer\.cs$/i, + /\.g\.cs$/i, + /\.g\.i\.cs$/i, + // OpenAPI / Swagger + /openapi.*\.gen\./i, + /swagger.*\.gen\./i, + // Prisma + /prisma\/client\//i, + // Generic patterns + /\/generated\//i, + /\/gen\//i, + /\/codegen\//i, +]; diff --git a/src/lib/index/syncer.ts b/src/lib/index/syncer.ts index dc540874..0726d6f2 100644 --- a/src/lib/index/syncer.ts +++ b/src/lib/index/syncer.ts @@ -7,8 +7,7 @@ import { VectorDB } from "../store/vector-db"; import { isIndexableFile } from "../utils/file-utils"; import { acquireWriterLockWithRetry, type LockHandle } from "../utils/lock"; import { ensureProjectPaths } from "../utils/project-root"; -import { getWorkerPool } from "../workers/pool"; -import type { ProcessFileResult } from "../workers/worker"; +import { processFile, type ProcessFileResult } from "../workers/orchestrator"; import type { InitialSyncProgress, InitialSyncResult } from "./sync-helpers"; import { walk } from "./walker"; @@ -119,7 +118,6 @@ export async function initialSync( let total = 0; onProgress?.({ processed: 0, indexed: 0, total, filePath: "Scanning..." }); - const pool = getWorkerPool(); const cachedPaths = dryRun || treatAsEmptyCache ? new Set() @@ -187,28 +185,15 @@ export async function initialSync( await flushLock; }; - const isTimeoutError = (err: unknown) => - err instanceof Error && err.message?.toLowerCase().includes("timed out"); - const processFileWithRetry = async ( relPath: string, absPath: string, ): Promise => { - let retries = 0; - while (true) { - try { - return await pool.processFile({ - path: relPath, - absolutePath: absPath, - }); - } catch (err) { - if (isTimeoutError(err) && retries === 0) { - retries += 1; - continue; - } - throw err; - } - } + // No retries needed - native embedding is stable + return await processFile({ + path: relPath, + absolutePath: absPath, + }); }; const schedule = async (task: () => Promise) => { diff --git a/src/lib/native/index.ts b/src/lib/native/index.ts new file mode 100644 index 00000000..a331eeab --- /dev/null +++ b/src/lib/native/index.ts @@ -0,0 +1,268 @@ + + +import { CONFIG, MODEL_IDS } from "../../config"; + +// Try to load native binding +let native: typeof import("osgrep-core") | null = null; +let initPromise: Promise | null = null; +let initialized = false; + +async function loadNative() { + if (native) return native; + + try { + native = await import("osgrep-core"); + } catch (e) { + throw new Error( + `Failed to load osgrep-core native binding. Run 'npm run build:release' in osgrep/osgrep-core/: ${e}` + ); + } + + return native; +} + +/** + * Initialize native models. Call once at startup. + */ +export async function initNative(): Promise { + if (initialized) return; + if (initPromise) return initPromise; + + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + + initPromise = (async () => { + if (DEBUG_TIMING) console.time("[native] loadNative"); + const n = await loadNative(); + if (DEBUG_TIMING) console.timeEnd("[native] loadNative"); + + if (!n.isInitialized()) { + if (DEBUG_TIMING) console.time("[native] initModels"); + n.initModels(MODEL_IDS.embed, MODEL_IDS.colbert); + if (DEBUG_TIMING) console.timeEnd("[native] initModels"); + } + + initialized = true; + })().finally(() => { + initPromise = null; + }); + + return initPromise; +} + +/** + * Check if native models are initialized + */ +export function isNativeInitialized(): boolean { + return initialized && native?.isInitialized() === true; +} + +// ============================================================================= +// Dense Embeddings +// ============================================================================= + +export interface DenseEmbedding { + vector: Float32Array; +} + +/** + * Embed texts to dense vectors (384-dim, L2-normalized) + */ +export async function embedDense(texts: string[]): Promise { + await initNative(); + const n = await loadNative(); + + const result = n.embedDense(texts); + const dim = CONFIG.VECTOR_DIM; + + // Split flat array into per-text vectors + const vectors: Float32Array[] = []; + for (let i = 0; i < result.count; i++) { + const start = i * dim; + const vec = new Float32Array(dim); + for (let j = 0; j < dim; j++) { + vec[j] = result.embeddings[start + j]; + } + vectors.push(vec); + } + + return vectors; +} + +// ============================================================================= +// ColBERT Embeddings (for indexing) +// ============================================================================= + +export interface ColbertPacked { + /** Quantized embeddings as Int8Array */ + embeddings: Int8Array; + /** Number of tokens per document */ + lengths: Uint32Array; + /** Byte offsets for each document */ + offsets: Uint32Array; +} + +/** + * Embed texts to ColBERT format (48-dim per token, INT8 quantized) + * Use at INDEX TIME to pre-compute embeddings + */ +export async function embedColbert(texts: string[]): Promise { + await initNative(); + const n = await loadNative(); + + const result = n.embedColbertPacked(texts); + + return { + embeddings: new Int8Array(result.embeddings), + lengths: new Uint32Array(result.lengths), + offsets: new Uint32Array(result.offsets), + }; +} + +// ============================================================================= +// Combined Embedding (for indexing) +// ============================================================================= + +export interface HybridEmbedding { + dense: Float32Array; + colbert: Int8Array; + token_ids: Uint32Array; + colbertLength: number; + colbertOffset: number; +} + +/** + * Embed texts for indexing (both dense and ColBERT in one call) + * Returns per-text embeddings ready for storage + */ +export async function embedBatch(texts: string[]): Promise { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + await initNative(); + const n = await loadNative(); + + if (DEBUG_TIMING) console.time(`[native] embedBatch (${texts.length} texts)`); + const result = n.embedBatch(texts); + if (DEBUG_TIMING) console.timeEnd(`[native] embedBatch (${texts.length} texts)`); + const dim = CONFIG.VECTOR_DIM; + const colbertDim = CONFIG.COLBERT_DIM; + + const embeddings: HybridEmbedding[] = []; + const tokenIds = new Uint32Array(result.colbertTokenIds); + let tokenCursor = 0; + + for (let i = 0; i < texts.length; i++) { + // Extract dense vector + const denseStart = i * dim; + const dense = new Float32Array(dim); + for (let j = 0; j < dim; j++) { + dense[j] = result.dense[denseStart + j]; + } + + // Extract ColBERT embedding for this doc + const colbertOffset = result.colbertOffsets[i]; + const colbertLength = result.colbertLengths[i]; + const colbertSize = colbertLength * colbertDim; + const colbert = new Int8Array(colbertSize); + for (let j = 0; j < colbertSize; j++) { + colbert[j] = result.colbertEmbeddings[colbertOffset + j]; + } + + // Extract token IDs for this doc (length = colbertLength) + const tokenSlice = tokenIds.subarray(tokenCursor, tokenCursor + colbertLength); + tokenCursor += colbertLength; + + embeddings.push({ + dense, + colbert, + token_ids: new Uint32Array(tokenSlice), + colbertLength, + colbertOffset: 0, // Will be set when storing + }); + } + + return embeddings; +} + +// ============================================================================= +// ColBERT Query Encoding +// ============================================================================= + +/** + * Encode query for ColBERT reranking + * Returns query embedding matrix as Float32Array + */ +export async function encodeQueryColbert(query: string): Promise { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + await initNative(); + const n = await loadNative(); + + if (DEBUG_TIMING) console.time("[native] encodeQueryColbert"); + const result = n.encodeQueryColbert(query); + if (DEBUG_TIMING) console.timeEnd("[native] encodeQueryColbert"); + return new Float32Array(result); +} + +// ============================================================================= +// ColBERT Reranking +// ============================================================================= + +export interface RerankInput { + /** Query ColBERT embedding from encodeQueryColbert */ + queryEmbedding: Float32Array; + /** Packed ColBERT doc embeddings (INT8) */ + docEmbeddings: Int8Array; + /** Packed ColBERT doc token ids (UINT32) aligned to docEmbeddings */ + docTokenIds: Uint32Array; + /** Token counts per doc */ + docLengths: number[]; + /** Byte offsets per doc */ + docOffsets: number[]; + /** Which doc indices to rerank */ + candidateIndices: number[]; + /** How many to return */ + topK: number; +} + +export interface RerankResult { + /** Original indices of top-k docs */ + indices: number[]; + /** MaxSim scores */ + scores: number[]; +} + +/** + * Rerank documents using pre-indexed ColBERT embeddings + */ +export async function rerankColbert(input: RerankInput): Promise { + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + await initNative(); + const n = await loadNative(); + + const q = Float64Array.from(input.queryEmbedding as any); + + const docs = + input.docEmbeddings instanceof Int8Array + ? input.docEmbeddings + : new Int8Array(input.docEmbeddings as any); + + const tokenIds = + input.docTokenIds instanceof Uint32Array + ? input.docTokenIds + : Uint32Array.from(input.docTokenIds as any); + + if (DEBUG_TIMING) console.time(`[native] rerankColbert (${input.candidateIndices.length} docs)`); + const result = n.rerankColbert( + q, + docs, + tokenIds, + input.docLengths, + input.docOffsets, + input.candidateIndices, + input.topK + ); + if (DEBUG_TIMING) console.timeEnd(`[native] rerankColbert (${input.candidateIndices.length} docs)`); + + return { + indices: Array.from(result.indices), + scores: Array.from(result.scores), + }; +} diff --git a/src/lib/output/formatter.ts b/src/lib/output/formatter.ts index 105e2b75..dc2d2647 100644 --- a/src/lib/output/formatter.ts +++ b/src/lib/output/formatter.ts @@ -1,18 +1,8 @@ import * as path from "node:path"; import { highlight } from "cli-highlight"; -import { getLanguageByExtension } from "../core/languages"; +import { getLanguageByExtension } from "../store/languages"; import type { ChunkType, FileMetadata } from "../store/types"; - -const useColors = process.stdout.isTTY && !process.env.NO_COLOR; - -const style = { - bold: (s: string) => (useColors ? `\x1b[1m${s}\x1b[22m` : s), - dim: (s: string) => (useColors ? `\x1b[2m${s}\x1b[22m` : s), - green: (s: string) => (useColors ? `\x1b[32m${s}\x1b[39m` : s), - blue: (s: string) => (useColors ? `\x1b[34m${s}\x1b[39m` : s), - cyan: (s: string) => (useColors ? `\x1b[36m${s}\x1b[39m` : s), - gray: (s: string) => (useColors ? `\x1b[90m${s}\x1b[39m` : s), -}; +import { style } from "../utils/ansi"; function detectLanguage(filePath: string): string { const ext = path.extname(filePath); @@ -117,50 +107,3 @@ export function formatResults( return results.map((r) => formatResult(r, root, options)).join("\n\n"); } -import type { GraphNode } from "../graph/graph-builder"; - -export function formatTrace(graph: { - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; -}): string { - if (!graph.center) { - return style.dim("Symbol not found."); - } - - const lines: string[] = []; - - // 1. Callers (Upstream) - if (graph.callers.length > 0) { - lines.push(style.bold("Callers (Who calls this?):")); - graph.callers.forEach((caller) => { - lines.push( - ` ${style.blue("↑")} ${style.green(caller.symbol)} ${style.dim(`(${caller.file}:${caller.line})`)}`, - ); - }); - lines.push(""); - } else { - lines.push(style.dim("No known callers.")); - lines.push(""); - } - - // 2. Center (The Symbol) - lines.push(style.bold("ā–¶ " + graph.center.symbol)); - lines.push( - ` ${style.dim(`Defined in ${graph.center.file}:${graph.center.line}`)}`, - ); - lines.push(` ${style.dim(`Role: ${graph.center.role}`)}`); - lines.push(""); - - // 3. Callees (Downstream) - if (graph.callees.length > 0) { - lines.push(style.bold("Callees (What does this call?):")); - graph.callees.forEach((callee) => { - lines.push(` ${style.cyan("↓")} ${callee}`); - }); - } else { - lines.push(style.dim("No known callees.")); - } - - return lines.join("\n"); -} diff --git a/src/lib/output/json-formatter.ts b/src/lib/output/json-formatter.ts deleted file mode 100644 index f9fccdc4..00000000 --- a/src/lib/output/json-formatter.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { GraphNode } from "../graph/graph-builder"; -import type { ChunkType } from "../store/types"; - -export interface JsonOutput { - results?: ChunkType[]; - hits?: unknown[]; - tsv?: string; - format?: string; - graph?: { - center: GraphNode | null; - callers: GraphNode[]; - callees: string[]; - }; - metadata?: { - count: number; - query?: string; - }; -} - -export function formatJson(data: JsonOutput): string { - return JSON.stringify(data, null, 2); -} diff --git a/src/lib/search/expander.ts b/src/lib/search/expander.ts new file mode 100644 index 00000000..bf70d178 --- /dev/null +++ b/src/lib/search/expander.ts @@ -0,0 +1,567 @@ +/** + * Expander - Retrieval augmentation for osgrep search results. + * + * Transforms search results from "find relevant chunks" to "build understanding context" + * by following symbol references, finding callers, and including neighboring files. + * + * Design Principles: + * 1. Hot Path Stays Hot - No overhead when --expand is not used + * 2. Language Agnostic - Uses existing Tree-sitter extracted symbols, no import parsing + * 3. Graceful Degradation - Partial expansion is fine, never fail the search + * 4. Token Budget Aware - Respects LLM context limits + */ + +import * as path from "node:path"; +import type { Table } from "@lancedb/lancedb"; +import type { ChunkType } from "../store/types"; +import type { VectorDB } from "../store/vector-db"; +import type { + ExpandedResult, + ExpandOptions, + ExpansionNode, + ExpansionStats, +} from "./expansion-types"; + +/** Default options for expansion */ +const DEFAULT_OPTIONS: Required = { + maxDepth: 1, + maxExpanded: 20, + maxTokens: 0, // 0 = unlimited + strategies: ["symbols", "callers", "neighbors"], +}; + +/** Tokens per character ratio (rough estimate for code) */ +const TOKENS_PER_CHAR = 0.25; + +/** + * Escape a string for use in SQL WHERE clauses. + */ +function escapeSql(str: string): string { + return str.replace(/'/g, "''"); +} + +/** + * Convert LanceDB array fields to string arrays. + */ +function toStrArray(val?: unknown): string[] { + if (!val) return []; + if (Array.isArray(val)) { + return val.filter((v) => typeof v === "string"); + } + if (typeof (val as any).toArray === "function") { + try { + const arr = (val as any).toArray(); + if (Array.isArray(arr)) return arr.filter((v) => typeof v === "string"); + return Array.from(arr || []).filter((v) => typeof v === "string"); + } catch { + return []; + } + } + return []; +} + +/** + * Estimate token count for a string. + */ +function estimateTokens(content: string): number { + return Math.ceil(content.length * TOKENS_PER_CHAR); +} + +/** + * Calculate file proximity score between two paths. + * Higher score = more related (same directory > nearby > distant). + */ +function fileProximity(fromPath: string, toPath: string): number { + if (fromPath === toPath) return 0; // Same file, skip + + const fromParts = fromPath.split("/"); + const toParts = toPath.split("/"); + + let common = 0; + for (let i = 0; i < Math.min(fromParts.length, toParts.length); i++) { + if (fromParts[i] === toParts[i]) common++; + else break; + } + + // More common path segments = higher score + return common / Math.max(fromParts.length, toParts.length); +} + +/** + * Extract import path hints from import strings. + * e.g., "../auth/handler" -> "auth/handler" + */ +function extractPathHint(importStr: string): string | null { + // Skip node_modules imports + if (!importStr.startsWith(".") && !importStr.startsWith("/")) return null; + + // Remove relative prefix and extension + return importStr + .replace(/^\.\.?\//, "") + .replace(/\.[^/.]+$/, "") + .split("/") + .slice(-2) + .join("/"); +} + +export class Expander { + constructor(private db: VectorDB) {} + + /** + * Expand search results to include related chunks. + * + * @param results Original search results + * @param query The original search query + * @param opts Expansion options + * @returns Expanded results with relationship metadata + */ + async expand( + results: ChunkType[], + query: string, + opts: ExpandOptions = {}, + ): Promise { + const options = { ...DEFAULT_OPTIONS, ...opts }; + const { maxDepth, maxExpanded, maxTokens, strategies } = options; + + const stats: ExpansionStats = { + symbolsResolved: 0, + callersFound: 0, + neighborsAdded: 0, + totalChunks: results.length, + totalTokens: 0, + }; + + // Track seen chunk IDs to avoid duplicates + const seen = new Set(); + for (const r of results) { + const id = this.getChunkId(r); + if (id) seen.add(id); + stats.totalTokens += estimateTokens(r.text || ""); + } + + // Token budget tracking + let budgetRemaining = maxTokens > 0 ? maxTokens - stats.totalTokens : Infinity; + + let table: Table; + try { + table = await this.db.ensureTable(); + } catch { + // Database not ready, return original results + return { + query, + original: results, + expanded: [], + truncated: false, + stats, + }; + } + + const allExpanded: ExpansionNode[] = []; + let truncated = false; + + // Multi-hop expansion using BFS + let frontier: { chunk: ChunkType; depth: number; score: number }[] = results.map( + (r) => ({ + chunk: r, + depth: 0, + score: 1.0, + }), + ); + + for (let depth = 1; depth <= maxDepth; depth++) { + if (truncated || budgetRemaining <= 0) break; + + const nextFrontier: typeof frontier = []; + + for (const node of frontier) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + + // Symbol resolution (Strategy 1) + if (strategies.includes("symbols")) { + const symbolNodes = await this.expandSymbols( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of symbolNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + nextFrontier.push({ chunk: n.chunk, depth, score: n.score }); + stats.symbolsResolved++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + + // Caller resolution (Strategy 2) + if (strategies.includes("callers") && !truncated) { + const callerNodes = await this.expandCallers( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of callerNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + nextFrontier.push({ chunk: n.chunk, depth, score: n.score }); + stats.callersFound++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + + // Neighbor expansion (Strategy 3) - only at depth 1 + if (strategies.includes("neighbors") && depth === 1 && !truncated) { + const neighborNodes = await this.expandNeighbors( + table, + node.chunk, + node.score, + depth, + seen, + maxExpanded - allExpanded.length, + budgetRemaining, + ); + + for (const n of neighborNodes) { + if (allExpanded.length >= maxExpanded || budgetRemaining <= 0) { + truncated = true; + break; + } + allExpanded.push(n); + stats.neighborsAdded++; + const tokens = estimateTokens(n.chunk.text || ""); + stats.totalTokens += tokens; + budgetRemaining -= tokens; + } + } + } + + frontier = nextFrontier; + } + + // Final sort by score (descending) + allExpanded.sort((a, b) => b.score - a.score); + + stats.totalChunks = results.length + allExpanded.length; + if (maxTokens > 0) { + stats.budgetRemaining = Math.max(0, budgetRemaining); + } + + return { + query, + original: results, + expanded: allExpanded, + truncated, + stats, + }; + } + + /** + * Expand by resolving referenced symbols to their definitions. + */ + private async expandSymbols( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set, + maxToAdd: number, + budgetRemaining: number, + ): Promise { + const refs = toStrArray(chunk.referenced_symbols); + if (refs.length === 0) return []; + + const chunkPath = this.getChunkPath(chunk); + const importHints = toStrArray(chunk.imports).map(extractPathHint).filter(Boolean) as string[]; + const expanded: ExpansionNode[] = []; + + // Limit symbols to process per chunk + const symbolsToProcess = refs.slice(0, 10); + + for (const symbol of symbolsToProcess) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + try { + // Query for chunks that define this symbol + const results = await table + .query() + .where(`array_contains(defined_symbols, '${escapeSql(symbol)}')`) + .limit(10) + .toArray(); + + // Sort by proximity to requesting file (import hints help disambiguate) + const sorted = results + .map((r: any) => ({ + record: r, + proximity: fileProximity(chunkPath, r.path || ""), + importMatch: importHints.some((h) => (r.path || "").includes(h)), + })) + .sort((a, b) => { + // Import matches first + if (a.importMatch !== b.importMatch) return b.importMatch ? 1 : -1; + // Then by proximity + return b.proximity - a.proximity; + }) + .slice(0, 3); + + for (const { record, proximity } of sorted) { + const id = record.id; + if (!id || seen.has(id)) continue; + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + const score = parentScore * 0.7 * (0.5 + proximity * 0.5); + + expanded.push({ + chunk: mappedChunk, + relationship: "symbols", + via: symbol, + depth, + score, + }); + + if (expanded.length >= maxToAdd) break; + } + } catch (err) { + // Graceful degradation - skip this symbol + continue; + } + } + + return expanded; + } + + /** + * Expand by finding callers (chunks that reference this chunk's defined symbols). + */ + private async expandCallers( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set, + maxToAdd: number, + budgetRemaining: number, + ): Promise { + const defined = toStrArray(chunk.defined_symbols); + if (defined.length === 0) return []; + + const chunkPath = this.getChunkPath(chunk); + const expanded: ExpansionNode[] = []; + + // Limit symbols to process + const symbolsToProcess = defined.slice(0, 5); + + for (const symbol of symbolsToProcess) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + try { + // Find chunks that reference this symbol + const results = await table + .query() + .where(`array_contains(referenced_symbols, '${escapeSql(symbol)}')`) + .limit(10) + .toArray(); + + // Sort by proximity + const sorted = results + .map((r: any) => ({ + record: r, + proximity: fileProximity(chunkPath, r.path || ""), + })) + .filter((x) => x.record.path !== chunkPath) // Exclude same file + .sort((a, b) => b.proximity - a.proximity) + .slice(0, 3); + + for (const { record, proximity } of sorted) { + const id = record.id; + if (!id || seen.has(id)) continue; + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + const score = parentScore * 0.6 * (0.5 + proximity * 0.5); + + expanded.push({ + chunk: mappedChunk, + relationship: "callers", + via: `uses ${symbol}`, + depth, + score, + }); + + if (expanded.length >= maxToAdd) break; + } + } catch { + continue; + } + } + + return expanded; + } + + /** + * Expand by including anchor chunks from the same directory. + */ + private async expandNeighbors( + table: Table, + chunk: ChunkType, + parentScore: number, + depth: number, + seen: Set, + maxToAdd: number, + budgetRemaining: number, + ): Promise { + const chunkPath = this.getChunkPath(chunk); + if (!chunkPath) return []; + + const dir = path.dirname(chunkPath); + const expanded: ExpansionNode[] = []; + + try { + // Get anchor chunks from same directory (file summaries) + const results = await table + .query() + .where(`path LIKE '${escapeSql(dir)}/%' AND is_anchor = true`) + .limit(5) + .toArray(); + + for (const record of results as any[]) { + if (expanded.length >= maxToAdd || budgetRemaining <= 0) break; + + const id = record.id; + if (!id || seen.has(id)) continue; + if (record.path === chunkPath) continue; // Skip same file + + const tokens = estimateTokens(record.content || record.display_text || ""); + if (tokens > budgetRemaining) continue; + + seen.add(id); + budgetRemaining -= tokens; + + const mappedChunk = this.mapRecordToChunk(record); + + expanded.push({ + chunk: mappedChunk, + relationship: "neighbors", + via: "same directory", + depth, + score: parentScore * 0.4, + }); + } + } catch { + // Graceful degradation + } + + return expanded; + } + + /** + * Get a unique ID for a chunk. + */ + private getChunkId(chunk: ChunkType): string { + if (chunk.metadata && typeof (chunk.metadata as any).hash === "string") { + return (chunk.metadata as any).hash; + } + const p = this.getChunkPath(chunk); + const start = chunk.generated_metadata?.start_line ?? 0; + return `${p}:${start}`; + } + + /** + * Get the file path from a chunk. + */ + private getChunkPath(chunk: ChunkType): string { + if (chunk.metadata && typeof (chunk.metadata as any).path === "string") { + return (chunk.metadata as any).path; + } + return ""; + } + + /** + * Map a LanceDB record to ChunkType. + */ + private mapRecordToChunk(record: any): ChunkType { + const startLine = record.start_line ?? 0; + const endLine = typeof record.end_line === "number" ? record.end_line : startLine; + const numLines = Math.max(1, endLine - startLine + 1); + + // Clean content (strip headers) + const content = record.display_text || record.content || ""; + const lines = content.split("\n"); + let startIdx = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) continue; + if (line.startsWith("// File:") || line.startsWith("File:")) continue; + if (line.startsWith("Imports:") || line.startsWith("Exports:")) continue; + if (line === "---" || line === "(anchor)") continue; + if (line.startsWith("//")) continue; + startIdx = i; + break; + } + + const bodyLines = lines.slice(startIdx); + const MAX_LINES = 15; + let truncatedText = bodyLines.slice(0, MAX_LINES).join("\n"); + if (bodyLines.length > MAX_LINES) { + truncatedText += `\n... (+${bodyLines.length - MAX_LINES} more lines)`; + } + + return { + type: "text", + text: truncatedText.trim(), + score: 0, + metadata: { + path: record.path || "", + hash: record.hash || "", + is_anchor: !!record.is_anchor, + }, + generated_metadata: { + start_line: startLine, + end_line: endLine, + num_lines: numLines, + type: record.chunk_type, + }, + complexity: record.complexity, + is_exported: record.is_exported, + role: record.role, + parent_symbol: record.parent_symbol, + defined_symbols: toStrArray(record.defined_symbols), + referenced_symbols: toStrArray(record.referenced_symbols), + imports: toStrArray(record.imports), + exports: toStrArray(record.exports), + }; + } +} diff --git a/src/lib/search/expansion-types.ts b/src/lib/search/expansion-types.ts new file mode 100644 index 00000000..1e5b7c9b --- /dev/null +++ b/src/lib/search/expansion-types.ts @@ -0,0 +1,101 @@ +/** + * Types for the retrieval augmentation (--expand) feature. + * + * This feature transforms osgrep from "find relevant chunks" to + * "build understanding context" by following symbol references, + * finding callers, and including neighboring files. + */ + +import type { ChunkType } from "../store/types"; + +/** + * Strategy for expanding search results. + * - symbols: Follow referenced_symbols → defined_symbols + * - callers: Reverse lookup - who references my defined_symbols + * - neighbors: Same directory files (anchor chunks) + * - coexports: Files that export symbols I import + */ +export type ExpandStrategy = "symbols" | "callers" | "neighbors" | "coexports"; + +/** + * Options for result expansion. + */ +export interface ExpandOptions { + /** Maximum traversal depth (default: 1) */ + maxDepth?: number; + /** Maximum number of expanded chunks to return (default: 20) */ + maxExpanded?: number; + /** Maximum token budget for expanded results (default: unlimited) */ + maxTokens?: number; + /** Which strategies to use (default: all) */ + strategies?: ExpandStrategy[]; +} + +/** + * A single expanded chunk with relationship metadata. + */ +export interface ExpansionNode { + /** The actual chunk data */ + chunk: ChunkType; + /** How this chunk is related to the original results */ + relationship: ExpandStrategy; + /** Human-readable explanation of the relationship */ + via: string; + /** How many hops from the original result */ + depth: number; + /** Relevance score (higher = more relevant, decays with depth) */ + score: number; +} + +/** + * Statistics about the expansion process. + */ +export interface ExpansionStats { + /** Number of symbols successfully resolved to definitions */ + symbolsResolved: number; + /** Number of callers found */ + callersFound: number; + /** Number of neighbor files added */ + neighborsAdded: number; + /** Total chunks in the expanded result */ + totalChunks: number; + /** Estimated total tokens in the result */ + totalTokens: number; + /** Remaining token budget (if maxTokens was set) */ + budgetRemaining?: number; +} + +/** + * The result of expanding search results. + */ +export interface ExpandedResult { + /** The original search query */ + query: string; + /** Original search results (unchanged) */ + original: ChunkType[]; + /** Expanded chunks with relationship metadata */ + expanded: ExpansionNode[]; + /** Whether any limit was hit during expansion */ + truncated: boolean; + /** Expansion statistics */ + stats: ExpansionStats; +} + +/** + * Internal representation of a chunk for expansion processing. + * Contains the raw data needed for symbol/caller resolution. + */ +export interface ExpansionChunk { + id: string; + path: string; + startLine: number; + endLine: number; + content: string; + definedSymbols: string[]; + referencedSymbols: string[]; + imports: string[]; + exports: string[]; + isAnchor: boolean; + role?: string; + complexity?: number; +} diff --git a/src/lib/search/intent.ts b/src/lib/search/intent.ts deleted file mode 100644 index f12edce6..00000000 --- a/src/lib/search/intent.ts +++ /dev/null @@ -1,34 +0,0 @@ -export interface SearchIntent { - type: "DEFINITION" | "FLOW" | "USAGE" | "ARCHITECTURE" | "GENERAL"; - filters?: { - definitionsOnly?: boolean; - usagesOnly?: boolean; - }; - mode?: "orchestration_first" | "show_examples" | "group_by_role"; -} - -export function detectIntent(query: string): SearchIntent { - const normalized = query.toLowerCase(); - - // Definition queries - if (/where is|what is|define/.test(normalized)) { - return { type: "DEFINITION", filters: { definitionsOnly: true } }; - } - - // Implementation queries - if (/how does|how is|implementation/.test(normalized)) { - return { type: "FLOW", mode: "orchestration_first" }; - } - - // Usage queries - if (/example|how to use|usage/.test(normalized)) { - return { type: "USAGE", mode: "show_examples" }; - } - - // Architecture queries - if (/architecture|system|overview/.test(normalized)) { - return { type: "ARCHITECTURE", mode: "group_by_role" }; - } - - return { type: "GENERAL" }; -} diff --git a/src/lib/search/searcher.ts b/src/lib/search/searcher.ts index 040a4297..c72c3f32 100644 --- a/src/lib/search/searcher.ts +++ b/src/lib/search/searcher.ts @@ -1,5 +1,6 @@ import type { Table } from "@lancedb/lancedb"; import { CONFIG } from "../../config"; +import { GENERATED_FILE_PATTERNS } from "../index/ignore-patterns"; import type { ChunkType, SearchFilter, @@ -8,12 +9,74 @@ import type { } from "../store/types"; import type { VectorDB } from "../store/vector-db"; import { escapeSqlString, normalizePath } from "../utils/filter-builder"; -import { getWorkerPool } from "../workers/pool"; -import { detectIntent, type SearchIntent } from "./intent"; +import { encodeQuery, rerank } from "../workers/orchestrator"; +import { Expander } from "./expander"; +import type { ExpandedResult, ExpandOptions } from "./expansion-types"; export class Searcher { constructor(private db: VectorDB) {} + private static readonly PRE_RERANK_K_MULT = 5; + private static readonly PRE_RERANK_K_MIN = 500; + private static readonly RERANK_CANDIDATES_K = 80; + private static readonly FUSED_WEIGHT = 0.5; + private static readonly MAX_PER_FILE = 3; + private static readonly FTS_STOPWORDS = new Set([ + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "before", + "but", + "by", + "do", + "does", + "for", + "from", + "how", + "i", + "if", + "in", + "into", + "is", + "it", + "of", + "on", + "or", + "our", + "that", + "the", + "their", + "then", + "this", + "to", + "we", + "what", + "when", + "where", + "which", + "who", + "why", + "with", + ]); + + private static normalizeFtsQuery(query: string): string { + const tokens = query + .toLowerCase() + .replace(/[^a-z0-9_]+/g, " ") + .split(/\s+/g) + .map((t) => t.trim()) + .filter(Boolean) + .filter((t) => t.length >= 3) + .filter((t) => !Searcher.FTS_STOPWORDS.has(t)); + + // Keep the query short to avoid pathological FTS parsing / scoring. + return tokens.slice(0, 16).join(" "); + } + private mapRecordToChunk( record: Partial, score: number, @@ -132,104 +195,49 @@ export class Searcher { }; } + private static readonly GENERATED_PENALTY = 0.4; + private applyStructureBoost( record: Partial, score: number, - intent?: SearchIntent, ): number { let adjusted = score; - // Item 6: Anchors are recall helpers, not rank contenders + // Anchor penalty (anchors are recall helpers, not results) if (record.is_anchor) { - // Minimal penalty to break ties - const anchorPenalty = - Number.parseFloat(process.env.OSGREP_ANCHOR_PENALTY ?? "") || 0.99; - adjusted *= anchorPenalty; - } else { - // Only boost non-anchors - const chunkType = record.chunk_type || ""; - const boosted = [ - "function", - "class", - "method", - "interface", - "type_alias", - ]; - if (boosted.includes(chunkType)) { - let boostFactor = 1.0; - - // Base boost - boostFactor *= 1.1; - - // --- Role Boost --- - if (record.role === "ORCHESTRATION") { - boostFactor *= 1.5; - } else if (record.role === "DEFINITION") { - boostFactor *= 1.2; - } else if (record.role === "IMPLEMENTATION") { - boostFactor *= 1.1; - } - - // --- Complexity/Orchestration Boost (User Requested) --- - const refs = record.referenced_symbols?.length || 0; - - if (refs > 5) { - // Small boost for non-trivial functions - boostFactor *= 1.1; - } - if (refs > 15) { - // Massive boost for Orchestrators - boostFactor *= 1.25; - } - - // Intent-based boosts - if (intent) { - if (intent.type === "DEFINITION" && record.role === "DEFINITION") { - boostFactor *= 1.2; - } - if (intent.type === "FLOW" && record.role === "ORCHESTRATION") { - boostFactor *= 1.4; - } - if (intent.type === "USAGE" && record.role === "IMPLEMENTATION") { - boostFactor *= 1.2; - } - } - - adjusted *= boostFactor; - } - } - - if (record.role === "DOCS") { - adjusted *= 0.6; + adjusted *= 0.99; } const pathStr = (record.path || "").toLowerCase(); + // Generated file penalty - auto-generated code rarely contains business logic + const isGenerated = GENERATED_FILE_PATTERNS.some((pattern) => + pattern.test(pathStr), + ); + if (isGenerated) { + adjusted *= Searcher.GENERATED_PENALTY; + } + // Use path-segment and filename patterns to avoid false positives like "latest" const isTestPath = /(^|\/)(__tests__|tests?|specs?|benchmark)(\/|$)/i.test(pathStr) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(pathStr); if (isTestPath) { - const testPenalty = - Number.parseFloat(process.env.OSGREP_TEST_PENALTY ?? "") || 0.5; - adjusted *= testPenalty; + adjusted *= 0.5; + } + // Tooling/docs-like paths often contain lots of "how do we..." text and can + // dominate semantic queries (e.g., `tools/eval.ts`). + if (/(^|\/)(tools|scripts|experiments)(\/|$)/i.test(pathStr)) { + adjusted *= 0.35; } if ( pathStr.endsWith(".md") || - pathStr.endsWith(".mdx") || - pathStr.endsWith(".txt") || pathStr.endsWith(".json") || pathStr.endsWith(".lock") || pathStr.includes("/docs/") ) { - const docPenalty = - Number.parseFloat(process.env.OSGREP_DOC_PENALTY ?? "") || 0.6; - adjusted *= docPenalty; // Downweight docs/data - } - // Import-only penalty - if ((record.content || "").length < 50 && !record.is_exported) { - adjusted *= 0.9; + adjusted *= 0.6; } return adjusted; @@ -278,22 +286,26 @@ export class Searcher { return deduped; } - private ftsIndexChecked = false; - async search( query: string, top_k?: number, - _search_options?: { rerank?: boolean }, - _filters?: SearchFilter, + options?: { rerank?: boolean }, + filters?: SearchFilter, pathPrefix?: string, - intent?: SearchIntent, signal?: AbortSignal, ): Promise { - const finalLimit = top_k ?? 10; - const doRerank = _search_options?.rerank ?? true; - const searchIntent = intent || detectIntent(query); + const DEBUG_TIMING = process.env.DEBUG_SEARCH_TIMING === "1"; + const DEBUG_ABORT = process.env.DEBUG_SERVER === "1" || process.env.DEBUG_ABORT === "1"; + const t = (label: string) => DEBUG_TIMING && console.time(`[search] ${label}`); + const te = (label: string) => DEBUG_TIMING && console.timeEnd(`[search] ${label}`); - const pool = getWorkerPool(); + t("total"); + const finalLimitRaw = top_k ?? 10; + const finalLimit = + Number.isFinite(finalLimitRaw) && finalLimitRaw > 0 ? finalLimitRaw : 10; + const doRerank = options?.rerank ?? true; + + if (DEBUG_ABORT) console.log(`[searcher] start, signal.aborted=${signal?.aborted}`); if (signal?.aborted) { const err = new Error("Aborted"); @@ -301,12 +313,15 @@ export class Searcher { throw err; } + t("encodeQuery"); + if (DEBUG_ABORT) console.log("[searcher] before encodeQuery"); const { dense: queryVector, colbert: queryMatrixRaw, colbertDim, - pooled_colbert_48d: queryPooled, - } = await pool.encodeQuery(query, signal); + } = await encodeQuery(query); + te("encodeQuery"); + if (DEBUG_ABORT) console.log(`[searcher] after encodeQuery, signal.aborted=${signal?.aborted}`); if (signal?.aborted) { const err = new Error("Aborted"); @@ -328,69 +343,60 @@ export class Searcher { } // Handle --def (definition) filter - const defFilter = _filters?.def; + const defFilter = filters?.def; if (typeof defFilter === "string" && defFilter) { whereClauseParts.push( `array_contains(defined_symbols, '${escapeSqlString(defFilter)}')`, ); - } else if ( - searchIntent.type === "DEFINITION" && - searchIntent.filters?.definitionsOnly - ) { - // If intent is DEFINITION but no specific symbol provided, filter by role - whereClauseParts.push( - `(role = 'DEFINITION' OR array_length(defined_symbols) > 0)`, - ); } // Handle --ref (reference) filter - const refFilter = _filters?.ref; + const refFilter = filters?.ref; if (typeof refFilter === "string" && refFilter) { whereClauseParts.push( `array_contains(referenced_symbols, '${escapeSqlString(refFilter)}')`, ); - } else if ( - searchIntent.type === "USAGE" && - searchIntent.filters?.usagesOnly - ) { - // If intent is USAGE, we might want to filter out definitions? - // For now, let's just rely on boosting. } const whereClause = whereClauseParts.length > 0 ? whereClauseParts.join(" AND ") : undefined; - const envPreK = Number.parseInt(process.env.OSGREP_PRE_K ?? "", 10); - const PRE_RERANK_K = - Number.isFinite(envPreK) && envPreK > 0 - ? envPreK - : Math.max(finalLimit * 5, 500); + const PRE_RERANK_K = Math.max( + finalLimit * Searcher.PRE_RERANK_K_MULT, + Searcher.PRE_RERANK_K_MIN, + ); + t("ensureTable"); let table: Table; try { table = await this.db.ensureTable(); } catch { + te("ensureTable"); + te("total"); return { data: [] }; } + te("ensureTable"); - // Ensure FTS index exists (lazy init on first search) - if (!this.ftsIndexChecked) { - this.ftsIndexChecked = true; // Set immediately to prevent retry spam - try { - await this.db.createFTSIndex(); - } catch (e) { - console.warn("[Searcher] Failed to ensure FTS index:", e); - } - } + // Skip FTS index check during search - it should be created during indexing. + // The createFTSIndex call takes ~500ms even when index exists due to LanceDB overhead. + // If FTS search fails below, we fall back gracefully. + t("vectorSearch"); + if (DEBUG_ABORT) console.log("[searcher] before vectorSearch"); let vectorQuery = table.vectorSearch(queryVector).limit(PRE_RERANK_K); if (whereClause) { vectorQuery = vectorQuery.where(whereClause); } const vectorResults = (await vectorQuery.toArray()) as VectorRecord[]; + te("vectorSearch"); + if (DEBUG_ABORT) console.log(`[searcher] after vectorSearch (${vectorResults.length} results), signal.aborted=${signal?.aborted}`); + t("ftsSearch"); + if (DEBUG_ABORT) console.log("[searcher] before ftsSearch"); let ftsResults: VectorRecord[] = []; try { - let ftsQuery = table.search(query).limit(PRE_RERANK_K); + const ftsText = Searcher.normalizeFtsQuery(query); + if (!ftsText) throw new Error("Empty FTS query after normalization"); + let ftsQuery = table.search(ftsText).limit(PRE_RERANK_K); if (whereClause) { ftsQuery = ftsQuery.where(whereClause); } @@ -399,6 +405,8 @@ export class Searcher { const msg = e instanceof Error ? e.message : String(e); console.warn(`[Searcher] FTS search failed: ${msg}`); } + te("ftsSearch"); + if (DEBUG_ABORT) console.log(`[searcher] after ftsSearch (${ftsResults.length} results), signal.aborted=${signal?.aborted}`); if (signal?.aborted) { const err = new Error("Aborted"); @@ -430,74 +438,27 @@ export class Searcher { .map(([key]) => docMap.get(key)) .filter(Boolean) as VectorRecord[]; - // Item 8: Widen PRE_RERANK_K - // Retrieve a wide set for Stage 1 filtering - const envStage1 = Number.parseInt(process.env.OSGREP_STAGE1_K ?? "", 10); - const STAGE1_K = - Number.isFinite(envStage1) && envStage1 > 0 ? envStage1 : 200; - const topCandidates = fused.slice(0, STAGE1_K); - - // Item 9: Two-stage rerank - // Stage 1: Cheap pooled cosine filter - let stage2Candidates = topCandidates; - const envStage2K = Number.parseInt(process.env.OSGREP_STAGE2_K ?? "", 10); - const STAGE2_K = - Number.isFinite(envStage2K) && envStage2K > 0 ? envStage2K : 40; - - const envRerankTop = Number.parseInt( - process.env.OSGREP_RERANK_TOP ?? "", - 10, - ); - const RERANK_TOP = - Number.isFinite(envRerankTop) && envRerankTop > 0 ? envRerankTop : 20; - const envBlend = Number.parseFloat(process.env.OSGREP_RERANK_BLEND ?? ""); - const FUSED_WEIGHT = - Number.isFinite(envBlend) && envBlend >= 0 ? envBlend : 0.5; - - if (queryPooled && topCandidates.length > STAGE2_K) { - const cosineScores = topCandidates.map((doc) => { - if (!doc.pooled_colbert_48d) return -1; - // Manual cosine sim since we don't have helper here easily - // Assuming vectors are normalized (which they should be from orchestrator) - let dot = 0; - const docVec = doc.pooled_colbert_48d; - for (let i = 0; i < queryPooled.length; i++) { - dot += queryPooled[i] * (docVec[i] || 0); - } - return dot; - }); - - // Sort by cosine score and keep top N - const withScore = topCandidates.map((doc, i) => ({ - doc, - score: cosineScores[i], - })); - withScore.sort((a, b) => b.score - a.score); - stage2Candidates = withScore.slice(0, STAGE2_K).map((x) => x.doc); - } - - if (stage2Candidates.length === 0) { + if (fused.length === 0) { return { data: [] }; } - const rerankCandidates = stage2Candidates.slice(0, RERANK_TOP); + const rerankCandidates = fused.slice(0, Searcher.RERANK_CANDIDATES_K); + t("rerank"); const scores = doRerank - ? await pool.rerank( - { + ? await rerank({ query: queryMatrixRaw, docs: rerankCandidates.map((doc) => ({ colbert: (doc.colbert as Buffer | Int8Array | number[]) ?? [], - scale: - typeof doc.colbert_scale === "number" ? doc.colbert_scale : 1, token_ids: Array.isArray((doc as any).doc_token_ids) ? ((doc as any).doc_token_ids as number[]) - : undefined, + : typeof (doc as any).doc_token_ids?.toArray === "function" + ? (((doc as any).doc_token_ids.toArray() as unknown[]) ?? []) + .filter((v) => typeof v === "number") as number[] + : [], })), colbertDim, - }, - signal, - ) + }) : rerankCandidates.map((doc, idx) => { // If rerank is disabled, fall back to fusion ordering with structural boost const key = doc.id || `${doc.path}:${doc.chunk_index}`; @@ -505,6 +466,7 @@ export class Searcher { // Small tie-breaker so later items don't all share 0 return fusedScore || 1 / (idx + 1); }); + te("rerank"); type ScoredItem = { record: (typeof rerankCandidates)[number]; @@ -515,8 +477,8 @@ export class Searcher { const base = scores?.[idx] ?? 0; const key = doc.id || `${doc.path}:${doc.chunk_index}`; const fusedScore = candidateScores.get(key) ?? 0; - const blended = base + FUSED_WEIGHT * fusedScore; - const boosted = this.applyStructureBoost(doc, blended, searchIntent); + const blended = base + Searcher.FUSED_WEIGHT * fusedScore; + const boosted = this.applyStructureBoost(doc, blended); return { record: doc, score: boosted }; }); @@ -529,17 +491,11 @@ export class Searcher { // Item 10: Per-file diversification const seenFiles = new Map(); const diversified: ScoredItem[] = []; - const envMaxPerFile = Number.parseInt( - process.env.OSGREP_MAX_PER_FILE ?? "", - 10, - ); - const MAX_PER_FILE = - Number.isFinite(envMaxPerFile) && envMaxPerFile > 0 ? envMaxPerFile : 3; for (const item of uniqueScored) { const path = item.record.path || ""; const count = seenFiles.get(path) || 0; - if (count < MAX_PER_FILE) { + if (count < Searcher.MAX_PER_FILE) { diversified.push(item); seenFiles.set(path, count + 1); } @@ -556,6 +512,7 @@ export class Searcher { // Item 12: Score Calibration const maxScore = finalResults.length > 0 ? finalResults[0]._score : 1.0; + te("total"); return { data: finalResults.map((r: (typeof finalResults)[number]) => { const chunk = this.mapRecordToChunk(r, r._score || 0); @@ -573,4 +530,64 @@ export class Searcher { }), }; } + + /** + * Expand search results to include related chunks. + * This is opt-in and adds no overhead when not used. + * + * @param results Original search results + * @param query The original search query + * @param opts Expansion options + * @returns Expanded results with relationship metadata + */ + async expand( + results: ChunkType[], + query: string, + opts?: ExpandOptions, + ): Promise { + const expander = new Expander(this.db); + return expander.expand(results, query, opts); + } + + /** + * Search and expand in one call. + * + * @param query Search query + * @param top_k Number of results + * @param searchOpts Search options + * @param filters Search filters + * @param pathPrefix Path prefix filter + * @param signal Abort signal + * @param expandOpts Expansion options (if provided, results will be expanded) + * @returns Search response with optional expansion + */ + async searchAndExpand( + query: string, + top_k?: number, + searchOpts?: { rerank?: boolean }, + filters?: SearchFilter, + pathPrefix?: string, + signal?: AbortSignal, + expandOpts?: ExpandOptions, + ): Promise { + const searchResult = await this.search( + query, + top_k, + searchOpts, + filters, + pathPrefix, + signal, + ); + + if (!expandOpts) { + return searchResult; + } + + const expanded = await this.expand(searchResult.data, query, expandOpts); + + return { + ...searchResult, + expanded, + }; + } } diff --git a/src/lib/setup/model-loader.ts b/src/lib/setup/model-loader.ts deleted file mode 100644 index 2d014fa9..00000000 --- a/src/lib/setup/model-loader.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { Worker } from "node:worker_threads"; -import { MODEL_IDS } from "../../config"; - -const HOMEDIR = os.homedir(); -const CACHE_DIR = path.join(HOMEDIR, ".osgrep", "models"); -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; - -/** - * Triggers the download of models by spawning a worker thread. - * This prevents the main thread from loading onnxruntime, avoiding exit crashes. - */ -export async function downloadModels(): Promise { - return new Promise((resolve, reject) => { - const tsWorkerPath = path.join(__dirname, "../workers/download-worker.ts"); - const jsWorkerPath = path.join(__dirname, "../workers/download-worker.js"); - const hasTsWorker = fs.existsSync(tsWorkerPath); - const hasJsWorker = fs.existsSync(jsWorkerPath); - const runningTs = path.extname(__filename) === ".ts"; - const isDev = (runningTs && hasTsWorker) || (hasTsWorker && !hasJsWorker); - - const workerPath = isDev ? tsWorkerPath : jsWorkerPath; - const execArgv = isDev ? ["-r", "ts-node/register"] : []; - - const worker = new Worker(workerPath, { execArgv }); - - worker.on("message", (msg) => { - if (msg.type === "progress") { - // Ignore progress messages for now, or log if debug enabled - return; - } - - if (msg.status === "success") { - if (LOG_MODELS) console.log("Worker: Models ready."); - resolve(); - } else if (msg.status === "error") { - reject(new Error(msg.error || "Unknown worker error")); - } - // Ignore other messages - }); - - worker.on("error", (err) => { - reject(err); - }); - - worker.on("exit", (code) => { - if (code !== 0) { - reject(new Error(`Download worker exited with code ${code}`)); - } - }); - }); -} - -/** - * Simple check to see if the cache folder exists for our models. - * This is a loose check for the UI/Doctor command. - */ -export function areModelsDownloaded(): boolean { - // Check if the model directories exist in the cache - const embedPath = path.join(CACHE_DIR, ...MODEL_IDS.embed.split("/")); - const colbertPath = path.join(CACHE_DIR, ...MODEL_IDS.colbert.split("/")); - - return fs.existsSync(embedPath) && fs.existsSync(colbertPath); -} diff --git a/src/lib/setup/setup-helpers.ts b/src/lib/setup/setup-helpers.ts index bbf7ddec..c5e26e8e 100644 --- a/src/lib/setup/setup-helpers.ts +++ b/src/lib/setup/setup-helpers.ts @@ -1,23 +1,19 @@ import * as fs from "node:fs"; import ora from "ora"; import { PATHS } from "../../config"; -import { areModelsDownloaded, downloadModels } from "./model-loader"; export interface SetupPaths { root: string; - models: string; grammars: string; } export interface SetupStatus extends SetupPaths { createdDirs: boolean; - downloadedModels: boolean; } function getPaths(): SetupPaths { return { root: PATHS.globalRoot, - models: PATHS.models, grammars: PATHS.grammars, }; } @@ -32,7 +28,7 @@ export async function ensureSetup({ silent?: boolean; } = {}): Promise { const paths = getPaths(); - const dirs = [paths.root, paths.models, paths.grammars]; + const dirs = [paths.root, paths.grammars]; const needsDirs = dirs.some((dir) => !fs.existsSync(dir)); let createdDirs = false; @@ -56,22 +52,5 @@ export async function ensureSetup({ throw error; } - const modelsPresent = areModelsDownloaded(); - let downloadedModels = false; - - if (!modelsPresent) { - const modelSpinner = !silent - ? ora("Downloading models (first run)...").start() - : null; - try { - await downloadModels(); - downloadedModels = true; - modelSpinner?.succeed("Models downloaded and ready"); - } catch (error) { - modelSpinner?.fail("Failed to download models"); - throw error; - } - } - - return { ...paths, createdDirs, downloadedModels }; + return { ...paths, createdDirs }; } diff --git a/src/lib/skeleton/body-fields.ts b/src/lib/skeleton/body-fields.ts deleted file mode 100644 index 5c32b33d..00000000 --- a/src/lib/skeleton/body-fields.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Body field mappings for TreeSitter AST nodes. - * - * Maps language -> node type -> body field name - * - string: The field name containing the body to elide - * - null: Node type has no body to elide (e.g., type aliases, interfaces) - * - undefined: Node type not recognized for this language - * - * IMPORTANT: Classes are "containers" - we don't elide their bodies, - * we recurse into them to skeletonize individual methods. - */ - -export const BODY_FIELDS: Record> = { - typescript: { - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - // Classes are containers - don't elide, recurse into them - class_declaration: null, - interface_declaration: null, // Interfaces have no body to elide - keep as-is - type_alias_declaration: null, // Type aliases are already compact - enum_declaration: null, // Enums are already compact - }, - - tsx: { - // Same as typescript - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - type_alias_declaration: null, - enum_declaration: null, - }, - - javascript: { - // Same as typescript (uses tsx grammar) - function_declaration: "body", - method_definition: "body", - arrow_function: "body", - generator_function_declaration: "body", - class_declaration: null, // Container - }, - - python: { - function_definition: "body", - class_definition: null, // Container - recurse into methods - }, - - go: { - function_declaration: "body", - method_declaration: "body", - type_declaration: null, // Type declarations are compact - }, - - rust: { - function_item: "body", - impl_item: null, // Container - recurse into methods - trait_item: null, // Trait definitions show method signatures - keep as-is - struct_item: null, // Struct definitions are compact - enum_item: null, // Enum definitions are compact - mod_item: "body", - }, - - java: { - method_declaration: "body", - constructor_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - enum_declaration: null, - }, - - c_sharp: { - method_declaration: "body", - constructor_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - struct_declaration: null, // Container - namespace_declaration: null, - }, - - cpp: { - function_definition: "body", - class_specifier: null, // Container - struct_specifier: null, // Container - namespace_definition: null, // Container - enum_specifier: null, - }, - - c: { - function_definition: "body", - struct_specifier: null, - enum_specifier: null, - }, - - ruby: { - method: "body", - class: null, // Container - module: null, // Container - singleton_method: "body", - }, - - php: { - function_definition: "body", - method_declaration: "body", - class_declaration: null, // Container - interface_declaration: null, - trait_declaration: null, // Container - }, -}; - -/** - * Container types - these hold methods/functions but shouldn't be elided themselves. - * We recurse into them to skeletonize their contents. - */ -export const CONTAINER_TYPES: Record = { - typescript: ["class_declaration", "class_body"], - tsx: ["class_declaration", "class_body"], - javascript: ["class_declaration", "class_body"], - python: ["class_definition"], - go: [], // Go doesn't have classes - rust: ["impl_item"], - java: ["class_declaration", "class_body"], - c_sharp: ["class_declaration", "struct_declaration", "class_body"], - cpp: ["class_specifier", "struct_specifier"], - c: [], - ruby: ["class", "module"], - php: ["class_declaration", "trait_declaration"], -}; - -/** - * Check if a node type is a container (holds methods). - */ -export function isContainerType(langId: string, nodeType: string): boolean { - return CONTAINER_TYPES[langId]?.includes(nodeType) ?? false; -} - -/** - * Get the body field name for a given language and node type. - * - * @returns string - Field name to access body - * @returns null - Node has no body to elide (keep as-is) - * @returns undefined - Node type not recognized - */ -export function getBodyField( - langId: string, - nodeType: string, -): string | null | undefined { - return BODY_FIELDS[langId]?.[nodeType]; -} - -/** - * Check if a node type has a body that can be elided. - */ -export function hasBodyField(langId: string, nodeType: string): boolean { - const field = getBodyField(langId, nodeType); - return typeof field === "string"; -} - -/** - * Check if a node type should be kept as-is (no elision). - * These are typically type definitions, interfaces, etc. - */ -export function shouldPreserveWhole(langId: string, nodeType: string): boolean { - const field = getBodyField(langId, nodeType); - return field === null; -} diff --git a/src/lib/skeleton/index.ts b/src/lib/skeleton/index.ts deleted file mode 100644 index 8039d518..00000000 --- a/src/lib/skeleton/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Skeleton module - Code compression for AI agents. - * - * Reduces token usage by 80-95% while preserving: - * - Function/method signatures - * - Class declarations - * - Type definitions - * - Summary of what functions do - */ - -export { - BODY_FIELDS, - getBodyField, - hasBodyField, - shouldPreserveWhole, -} from "./body-fields"; -export type { SkeletonOptions, SkeletonResult } from "./skeletonizer"; -export { Skeletonizer, skeletonizeFile } from "./skeletonizer"; -export type { ChunkMetadata, SummaryOptions } from "./summary-formatter"; -export { - formatSkeletonHeader, - formatSummary, - getCommentStyle, -} from "./summary-formatter"; diff --git a/src/lib/skeleton/retriever.ts b/src/lib/skeleton/retriever.ts deleted file mode 100644 index bc9b7651..00000000 --- a/src/lib/skeleton/retriever.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { VectorDB } from "../store/vector-db"; - -export async function getStoredSkeleton( - db: VectorDB, - filePath: string, -): Promise { - try { - const table = await db.ensureTable(); - // LanceDB query - const results = await table - .query() - .where(`path = '${filePath.replace(/'/g, "''")}' AND is_anchor = true`) - .limit(1) - .toArray(); - - if (results.length > 0) { - const skel = results[0].file_skeleton; - if (typeof skel === "string" && skel.length > 0) { - return skel; - } - } - return null; - } catch (e) { - // If table doesn't exist or query fails, return null - return null; - } -} diff --git a/src/lib/skeleton/skeletonizer.ts b/src/lib/skeleton/skeletonizer.ts deleted file mode 100644 index d53b8831..00000000 --- a/src/lib/skeleton/skeletonizer.ts +++ /dev/null @@ -1,562 +0,0 @@ -/** - * Skeletonizer - Compress code by replacing function bodies with summaries. - * - * Reduces token usage by 80-95% while preserving: - * - Function/method signatures - * - Class/interface declarations (structure preserved, methods skeletonized) - * - Type definitions - * - Decorators and annotations - * - Inline summaries of what functions do (calls, complexity, role) - */ - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getLanguageByExtension } from "../core/languages"; -import { GRAMMARS_DIR } from "../index/grammar-loader"; -import { getBodyField } from "./body-fields"; -import { - type ChunkMetadata, - formatSkeletonHeader, - formatSummary, - getCommentStyle, -} from "./summary-formatter"; - -// Import web-tree-sitter (CommonJS) -const TreeSitter = require("web-tree-sitter"); -const Parser = TreeSitter.Parser; -const Language = TreeSitter.Language; - -// TreeSitter types (matching chunker.ts) -interface TreeSitterNode { - type: string; - text: string; - startPosition: { row: number; column: number }; - endPosition: { row: number; column: number }; - startIndex: number; - endIndex: number; - namedChildren?: TreeSitterNode[]; - children?: TreeSitterNode[]; - parent?: TreeSitterNode; - childForFieldName?: (field: string) => TreeSitterNode | null; - previousSibling?: TreeSitterNode | null; -} - -interface TreeSitterParser { - setLanguage(language: TreeSitterLanguage): void; - parse(content: string): { rootNode: TreeSitterNode }; -} - -type TreeSitterLanguage = Record; - -// Result types -export interface SkeletonResult { - success: boolean; - skeleton: string; - tokenEstimate: number; - symbolCount: number; - language: string; - error?: string; -} - -export interface SkeletonOptions { - /** Preserve decorators/annotations in output. Default: true */ - preserveDecorators?: boolean; - /** Include summary comment in elided bodies. Default: true */ - includeSummary?: boolean; - /** Max function calls to show in summary. Default: 4 */ - maxCallsInSummary?: number; -} - -/** Represents a region to elide (replace with summary) */ -interface ElisionRegion { - bodyStart: number; - bodyEnd: number; - summary: string; - langId: string; -} - -const DEFAULT_OPTIONS: Required = { - preserveDecorators: true, - includeSummary: true, - maxCallsInSummary: 4, -}; - -// WASM locator (same as chunker.ts) -function resolveTreeSitterWasmLocator(): string { - try { - return require.resolve("web-tree-sitter/tree-sitter.wasm"); - } catch { - try { - const pkgDir = path.dirname( - require.resolve("web-tree-sitter/package.json"), - ); - const candidate = path.join(pkgDir, "tree-sitter.wasm"); - if (fs.existsSync(candidate)) return candidate; - } catch { - // fall through - } - return path.join( - __dirname, - "..", - "..", - "..", - "node_modules", - "web-tree-sitter", - "tree-sitter.wasm", - ); - } -} - -/** - * Main Skeletonizer class. - */ -export class Skeletonizer { - private parser: TreeSitterParser | null = null; - private languages: Map = new Map(); - private initialized = false; - - async init(): Promise { - if (this.initialized) return; - - try { - const wasmLocator = resolveTreeSitterWasmLocator(); - await Parser.init({ locator: wasmLocator }); - this.parser = new Parser() as TreeSitterParser; - } catch (_err) { - console.warn("āš ļø TreeSitter unavailable for skeletonization"); - this.parser = null; - } - - if (!fs.existsSync(GRAMMARS_DIR)) { - fs.mkdirSync(GRAMMARS_DIR, { recursive: true }); - } - - this.initialized = true; - } - - /** - * Check if a file can be skeletonized. - */ - isSupported(filePath: string): { - supported: boolean; - language?: string; - reason?: string; - } { - const ext = path.extname(filePath).toLowerCase(); - const langDef = getLanguageByExtension(ext); - - if (!langDef) { - return { - supported: false, - reason: `Unknown file extension: ${ext}`, - }; - } - - if (!langDef.grammar) { - return { - supported: false, - language: langDef.id, - reason: `No TreeSitter grammar for ${langDef.id}`, - }; - } - - return { - supported: true, - language: langDef.id, - }; - } - - /** - * Skeletonize a file. - */ - async skeletonizeFile( - filePath: string, - content: string, - options?: SkeletonOptions, - ): Promise { - if (!this.initialized) await this.init(); - - const opts = { ...DEFAULT_OPTIONS, ...options }; - const support = this.isSupported(filePath); - - // Handle unsupported languages - if (!support.supported) { - return this.createFallbackResult( - filePath, - content, - support.reason || "Unsupported language", - ); - } - - const ext = path.extname(filePath).toLowerCase(); - const langDef = getLanguageByExtension(ext); - if (!langDef?.grammar) { - return this.createFallbackResult( - filePath, - content, - "No grammar available", - ); - } - - // Load language - const language = await this.getLanguage(langDef.grammar.name); - if (!language || !this.parser) { - return this.createFallbackResult( - filePath, - content, - "Failed to load grammar", - ); - } - - try { - // Parse the file - this.parser.setLanguage(language); - const tree = this.parser.parse(content); - const root = tree.rootNode; - - // Find all regions to elide (function/method bodies) - const elisions: ElisionRegion[] = []; - this.findElisionRegions(root, langDef.id, content, elisions, opts); - - if (elisions.length === 0) { - // No functions found - return a compact version - return this.createFallbackResult( - filePath, - content, - "No functions/methods found", - ); - } - - // Sort by position (ascending) for correct reconstruction - elisions.sort((a, b) => a.bodyStart - b.bodyStart); - - // Build skeleton by replacing bodies with summaries - const skeleton = this.buildSkeleton(content, elisions, langDef.id); - const tokenEstimate = Math.ceil(skeleton.length / 4); - - return { - success: true, - skeleton: `${formatSkeletonHeader(filePath, tokenEstimate, langDef.id)}\n${skeleton}`, - tokenEstimate, - symbolCount: elisions.length, - language: langDef.id, - }; - } catch (err) { - return this.createFallbackResult( - filePath, - content, - `Parse error: ${err instanceof Error ? err.message : String(err)}`, - ); - } - } - - /** - * Find all regions that should be elided (function/method bodies). - * Recurses into containers (classes) to find methods. - */ - private findElisionRegions( - node: TreeSitterNode, - langId: string, - content: string, - elisions: ElisionRegion[], - opts: Required, - ): void { - // Check if this node has a body to elide - const bodyFieldName = getBodyField(langId, node.type); - - if (typeof bodyFieldName === "string") { - // This is a function/method - extract its body region - const bodyNode = node.childForFieldName?.(bodyFieldName); - if (bodyNode) { - const summary = this.createSummary(bodyNode, langId, opts); - elisions.push({ - bodyStart: bodyNode.startIndex, - bodyEnd: bodyNode.endIndex, - summary, - langId, - }); - // Don't recurse into the body - we're eliding it - return; - } - } - - // For containers (classes) or other nodes, recurse into children - const children = node.namedChildren || []; - for (const child of children) { - this.findElisionRegions(child, langId, content, elisions, opts); - } - } - - /** - * Create a summary comment for a function body. - */ - private createSummary( - bodyNode: TreeSitterNode, - langId: string, - opts: Required, - ): string { - if (!opts.includeSummary) { - return this.createElidedBody(langId, "// ..."); - } - - // Extract metadata from the body - const referencedSymbols = this.extractReferencedSymbols(bodyNode); - const complexity = this.calculateComplexity(bodyNode); - const role = this.classifyRole(complexity, referencedSymbols.length); - - const metadata: ChunkMetadata = { - referencedSymbols, - complexity, - role, - }; - - const commentStyle = getCommentStyle(langId); - const summaryLine = formatSummary(metadata, { - maxCalls: opts.maxCallsInSummary, - commentStyle, - }); - - return this.createElidedBody(langId, summaryLine); - } - - /** - * Create an elided body with summary for a specific language. - */ - private createElidedBody(langId: string, summary: string): string { - switch (langId) { - case "python": - // Python uses indented ... (Ellipsis) - valid syntax - return `\n ${summary}\n ...`; - case "ruby": - // Ruby body doesn't include closing 'end' in AST - it's preserved after - return `\n ${summary}`; - default: - // C-style languages use { ... } - return `{\n ${summary}\n }`; - } - } - - /** - * Build the skeleton by replacing function bodies with summaries. - */ - private buildSkeleton( - content: string, - elisions: ElisionRegion[], - _langId: string, - ): string { - const parts: string[] = []; - let cursor = 0; - - for (const elision of elisions) { - // Add content before this body - if (elision.bodyStart > cursor) { - parts.push(content.slice(cursor, elision.bodyStart)); - } - - // Add the elided body with summary - parts.push(elision.summary); - - cursor = elision.bodyEnd; - } - - // Add remaining content after last elision - if (cursor < content.length) { - parts.push(content.slice(cursor)); - } - - return this.cleanupSkeleton(parts.join("")); - } - - /** - * Clean up the skeleton output. - */ - private cleanupSkeleton(skeleton: string): string { - return ( - skeleton - // Remove excessive blank lines - .replace(/\n{3,}/g, "\n\n") - // Trim trailing whitespace on lines - .split("\n") - .map((line) => line.trimEnd()) - .join("\n") - .trim() - ); - } - - /** - * Create a fallback result when skeletonization isn't possible. - * Returns first 30 lines as preview. - */ - private createFallbackResult( - filePath: string, - content: string, - reason: string, - ): SkeletonResult { - const lines = content.split("\n"); - const previewLines = lines.slice(0, 30); - const truncated = lines.length > 30; - - const preview = [ - `// Skeleton unavailable: ${reason}`, - `// File: ${filePath}`, - `// Showing first ${Math.min(30, lines.length)} lines${truncated ? " (truncated)" : ""}`, - "", - ...previewLines, - ...(truncated ? ["", `// ... ${lines.length - 30} more lines`] : []), - ].join("\n"); - - return { - success: false, - skeleton: preview, - tokenEstimate: Math.ceil(preview.length / 4), - symbolCount: 0, - language: reason.includes("Unknown file extension") - ? path.extname(filePath) - : "unknown", - error: reason, - }; - } - - /** - * Load a TreeSitter language grammar. - */ - private async getLanguage(lang: string): Promise { - const cached = this.languages.get(lang); - if (cached !== undefined) return cached; - - const wasmPath = path.join(GRAMMARS_DIR, `tree-sitter-${lang}.wasm`); - if (!fs.existsSync(wasmPath)) { - this.languages.set(lang, null); - return null; - } - - try { - const language = Language - ? ((await Language.load(wasmPath)) as TreeSitterLanguage | null) - : null; - this.languages.set(lang, language); - return language; - } catch { - this.languages.set(lang, null); - return null; - } - } - - /** - * Extract referenced symbols (function calls) from a node. - */ - private extractReferencedSymbols(node: TreeSitterNode): string[] { - const refs: string[] = []; - const seen = new Set(); - - const extract = (n: TreeSitterNode) => { - if (n.type === "call_expression" || n.type === "call") { - const func = n.childForFieldName?.("function"); - if (func) { - let funcName = func.text; - - // Handle member access (obj.method) - extract just method - if (func.type === "member_expression") { - const prop = func.childForFieldName?.("property"); - if (prop) funcName = prop.text; - } else if (func.type === "attribute") { - const attr = func.childForFieldName?.("attribute"); - if (attr) funcName = attr.text; - } - - // Dedupe and filter noise - if (funcName && !seen.has(funcName) && funcName.length < 30) { - seen.add(funcName); - refs.push(funcName); - } - } - } else if ( - n.type === "method_invocation" || // Java - n.type === "invocation_expression" // C# - ) { - // Java/C# method calls - const nameNode = - n.childForFieldName?.("name") || n.childForFieldName?.("function"); - if (nameNode) { - refs.push(nameNode.text); - seen.add(nameNode.text); - } - } else if ( - n.type === "method_call" || // Ruby - n.type === "command" || // Ruby - n.type === "command_call" // Ruby - ) { - const nameNode = - n.childForFieldName?.("method") || n.childForFieldName?.("name"); - if (nameNode) { - refs.push(nameNode.text); - seen.add(nameNode.text); - } - } - - for (const child of n.namedChildren || []) { - extract(child); - } - }; - - extract(node); - return refs; - } - - /** - * Calculate cyclomatic complexity of a node. - */ - private calculateComplexity(node: TreeSitterNode): number { - let complexity = 1; - const complexTypes = [ - "if_statement", - "for_statement", - "while_statement", - "switch_statement", - "catch_clause", - "conditional_expression", - ]; - - const count = (n: TreeSitterNode) => { - if (complexTypes.includes(n.type)) { - complexity++; - } - if (n.type === "binary_expression") { - const op = n.childForFieldName?.("operator"); - if (["&&", "||", "??"].includes(op?.text || "")) { - complexity++; - } - } - for (const child of n.namedChildren || []) { - count(child); - } - }; - - count(node); - return complexity; - } - - /** - * Classify the role of a function based on its characteristics. - */ - private classifyRole(complexity: number, refCount: number): string { - // High complexity + many calls = orchestration - if (complexity > 5 && refCount > 5) { - return "ORCHESTRATION"; - } - return "IMPLEMENTATION"; - } -} - -/** - * Convenience function to skeletonize a file. - */ -export async function skeletonizeFile( - filePath: string, - content: string, - options?: SkeletonOptions, -): Promise { - const skeletonizer = new Skeletonizer(); - await skeletonizer.init(); - return skeletonizer.skeletonizeFile(filePath, content, options); -} diff --git a/src/lib/skeleton/summary-formatter.ts b/src/lib/skeleton/summary-formatter.ts deleted file mode 100644 index 575dc53b..00000000 --- a/src/lib/skeleton/summary-formatter.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Format inline summary comments for skeleton bodies. - * - * Output format: "// → call1, call2, call3 | C:8 | ORCH" - * - Shows referenced symbols (what the function calls) - * - Shows complexity score - * - Shows ORCH role only (the interesting one) - */ - -export interface ChunkMetadata { - referencedSymbols?: string[]; - complexity?: number; - role?: string; -} - -export interface SummaryOptions { - /** Maximum number of calls to show before truncating */ - maxCalls?: number; - /** Whether to show complexity */ - showComplexity?: boolean; - /** Whether to show role (only ORCH is shown) */ - showRole?: boolean; - /** Comment style for the language */ - commentStyle?: "slash" | "hash" | "dash"; -} - -const DEFAULT_OPTIONS: Required = { - maxCalls: 4, - showComplexity: true, - showRole: true, - commentStyle: "slash", -}; - -/** - * Format the inline summary comment for a function body. - * - * @example - * // Input: { referencedSymbols: ['findByEmail', 'compare', 'sign'], complexity: 8, role: 'ORCHESTRATION' } - * // Output: "// → findByEmail, compare, sign | C:8 | ORCH" - */ -export function formatSummary( - metadata: ChunkMetadata, - options: SummaryOptions = {}, -): string { - const opts = { ...DEFAULT_OPTIONS, ...options }; - const parts: string[] = []; - - // Calls (referenced symbols) - if (metadata.referencedSymbols?.length) { - const calls = metadata.referencedSymbols.slice(0, opts.maxCalls); - if (metadata.referencedSymbols.length > opts.maxCalls) { - calls.push("..."); - } - parts.push(`→ ${calls.join(", ")}`); - } - - // Complexity (only if > 1, trivial functions don't need it) - if (opts.showComplexity && metadata.complexity && metadata.complexity > 1) { - parts.push(`C:${metadata.complexity}`); - } - - // Role (only show ORCH - it's the architecturally interesting one) - if (opts.showRole && metadata.role === "ORCHESTRATION") { - parts.push("ORCH"); - } - - // Build the comment - const commentPrefix = getCommentPrefix(opts.commentStyle); - - if (parts.length === 0) { - return `${commentPrefix} ...`; - } - - return `${commentPrefix} ${parts.join(" | ")}`; -} - -/** - * Get the single-line comment prefix for a language style. - */ -function getCommentPrefix(style: "slash" | "hash" | "dash"): string { - switch (style) { - case "hash": - return "#"; - case "dash": - return "--"; - case "slash": - default: - return "//"; - } -} - -/** - * Get the appropriate comment style for a language. - */ -export function getCommentStyle(langId: string): "slash" | "hash" | "dash" { - switch (langId) { - case "python": - case "ruby": - case "bash": - return "hash"; - case "sql": - case "lua": - return "dash"; - default: - return "slash"; - } -} - -/** - * Format the skeleton file header comment. - */ -export function formatSkeletonHeader( - filePath: string, - tokenEstimate: number, - langId?: string, -): string { - const prefix = langId - ? getCommentPrefix(getCommentStyle(langId)) - : "//"; - return `${prefix} ${filePath} (skeleton, ~${tokenEstimate} tokens)`; -} diff --git a/src/lib/core/languages.ts b/src/lib/store/languages.ts similarity index 100% rename from src/lib/core/languages.ts rename to src/lib/store/languages.ts diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index a22b807c..76ea4f0a 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -31,8 +31,6 @@ export type PreparedChunk = { export type VectorRecord = PreparedChunk & { vector: Float32Array | number[]; colbert: Int8Array | Buffer | number[]; - colbert_scale: number; - pooled_colbert_48d?: Float32Array | number[]; doc_token_ids?: number[] | Int32Array; } & Record; diff --git a/src/lib/store/vector-db.ts b/src/lib/store/vector-db.ts index 0c940ef9..094a65a8 100644 --- a/src/lib/store/vector-db.ts +++ b/src/lib/store/vector-db.ts @@ -6,7 +6,6 @@ import { Field, FixedSizeList, Float32, - Float64, Int32, List, Schema, @@ -56,8 +55,6 @@ export class VectorDB { is_exported: false, vector: Array(CONFIG.VECTOR_DIM).fill(0), colbert: Buffer.alloc(0), - colbert_scale: 1, - pooled_colbert_48d: Array(CONFIG.COLBERT_DIM).fill(0), doc_token_ids: [], defined_symbols: [], referenced_symbols: [], @@ -108,15 +105,6 @@ export class VectorDB { new Field("complexity", new Float32(), true), new Field("is_exported", new Bool(), true), new Field("colbert", new Binary(), true), - new Field("colbert_scale", new Float64(), true), - new Field( - "pooled_colbert_48d", - new FixedSizeList( - CONFIG.COLBERT_DIM, - new Field("item", new Float32(), false), - ), - true, - ), new Field( "doc_token_ids", new List(new Field("item", new Int32(), true)), @@ -232,12 +220,7 @@ export class VectorDB { is_exported: rec.is_exported ?? false, vector: vec, colbert: toBuffer(rec.colbert), - colbert_scale: - typeof rec.colbert_scale === "number" ? rec.colbert_scale : 1, - pooled_colbert_48d: rec.pooled_colbert_48d - ? Array.from(rec.pooled_colbert_48d) - : undefined, - doc_token_ids: rec.doc_token_ids ? Array.from(rec.doc_token_ids) : null, + doc_token_ids: rec.doc_token_ids ? Array.from(rec.doc_token_ids) : [], defined_symbols: rec.defined_symbols ?? [], referenced_symbols: rec.referenced_symbols ?? [], imports: rec.imports ?? [], diff --git a/src/lib/utils/ansi.ts b/src/lib/utils/ansi.ts new file mode 100644 index 00000000..03c4fe5c --- /dev/null +++ b/src/lib/utils/ansi.ts @@ -0,0 +1,10 @@ +const useColors = process.stdout.isTTY && !process.env.NO_COLOR; + +export const style = { + bold: (s: string) => (useColors ? `\x1b[1m${s}\x1b[22m` : s), + dim: (s: string) => (useColors ? `\x1b[2m${s}\x1b[22m` : s), + green: (s: string) => (useColors ? `\x1b[32m${s}\x1b[39m` : s), + blue: (s: string) => (useColors ? `\x1b[34m${s}\x1b[39m` : s), + cyan: (s: string) => (useColors ? `\x1b[36m${s}\x1b[39m` : s), + gray: (s: string) => (useColors ? `\x1b[90m${s}\x1b[39m` : s), +}; diff --git a/src/lib/utils/exit.ts b/src/lib/utils/exit.ts index b9eac674..f498ae62 100644 --- a/src/lib/utils/exit.ts +++ b/src/lib/utils/exit.ts @@ -1,4 +1,3 @@ -import { destroyWorkerPool, isWorkerPoolInitialized } from "../workers/pool"; import { runCleanup } from "./cleanup"; export async function gracefulExit(code?: number): Promise { @@ -9,14 +8,6 @@ export async function gracefulExit(code?: number): Promise { ? process.exitCode : 0; - try { - if (isWorkerPoolInitialized()) { - await destroyWorkerPool(); - } - } catch (err) { - console.error("[exit] Failed to destroy worker pool:", err); - } - await runCleanup(); // Avoid exiting the process during test runs so Vitest can report results. diff --git a/src/lib/utils/formatter.ts b/src/lib/utils/formatter.ts index 33be0212..26b3022d 100644 --- a/src/lib/utils/formatter.ts +++ b/src/lib/utils/formatter.ts @@ -1,5 +1,7 @@ import * as path from "node:path"; import { highlight } from "cli-highlight"; +import { getLanguageByExtension } from "../store/languages"; +import { style } from "./ansi"; export interface TextResult { path: string; @@ -10,15 +12,6 @@ export interface TextResult { end_line: number; } -const style = { - bold: (s: string) => `\x1b[1m${s}\x1b[22m`, - dim: (s: string) => `\x1b[2m${s}\x1b[22m`, - green: (s: string) => `\x1b[32m${s}\x1b[39m`, - blue: (s: string) => `\x1b[34m${s}\x1b[39m`, -}; - -import { getLanguageByExtension } from "../core/languages"; - function detectLanguage(filePath: string): string { const ext = path.extname(filePath); const lang = getLanguageByExtension(ext); diff --git a/src/lib/utils/git.ts b/src/lib/utils/git.ts index 1ddc9f97..a16b44ca 100644 --- a/src/lib/utils/git.ts +++ b/src/lib/utils/git.ts @@ -1,11 +1,45 @@ import * as fs from "node:fs"; import * as path from "node:path"; +function realpathOrSelf(filePath: string): string { + try { + return fs.realpathSync.native(filePath); + } catch { + return filePath; + } +} + +function parseGitdirFromGitfile(gitFilePath: string): string | null { + try { + const content = fs.readFileSync(gitFilePath, "utf-8").trim(); + if (!content.startsWith("gitdir: ")) return null; + return content.slice(8).trim(); + } catch { + return null; + } +} + export function isWorktree(dir: string): boolean { const gitPath = path.join(dir, ".git"); try { const stats = fs.statSync(gitPath); - return stats.isFile(); + + // Standard worktree: `.git` is a gitfile pointing at `.git/worktrees/`. + if (stats.isFile()) { + const gitDir = parseGitdirFromGitfile(gitPath); + if (!gitDir) return false; + + const absGitDir = realpathOrSelf(path.resolve(dir, gitDir)); + return fs.existsSync(path.join(absGitDir, "commondir")); + } + + // Some tooling uses a symlinked directory for `.git` (e.g. external workspaces). + // Worktree git dirs include a `commondir` file; main repo `.git` typically does not. + if (stats.isDirectory()) { + return fs.existsSync(path.join(gitPath, "commondir")); + } + + return false; } catch { return false; } @@ -14,20 +48,34 @@ export function isWorktree(dir: string): boolean { export function getGitCommonDir(worktreeRoot: string): string | null { const gitPath = path.join(worktreeRoot, ".git"); try { - const content = fs.readFileSync(gitPath, "utf-8").trim(); - if (!content.startsWith("gitdir: ")) return null; + const stats = fs.statSync(gitPath); - const gitDir = content.slice(8).trim(); - const absGitDir = path.resolve(worktreeRoot, gitDir); + if (stats.isFile()) { + const gitDir = parseGitdirFromGitfile(gitPath); + if (!gitDir) return null; + + const absGitDir = realpathOrSelf(path.resolve(worktreeRoot, gitDir)); + + const commonDirFile = path.join(absGitDir, "commondir"); + if (fs.existsSync(commonDirFile)) { + const commonPath = fs.readFileSync(commonDirFile, "utf-8").trim(); + return realpathOrSelf(path.resolve(absGitDir, commonPath)); + } + + // Fallback: assume standard structure + return realpathOrSelf(path.resolve(absGitDir, "../../")); + } + + if (stats.isDirectory()) { + const commonDirFile = path.join(gitPath, "commondir"); + if (!fs.existsSync(commonDirFile)) return null; - const commonDirFile = path.join(absGitDir, "commondir"); - if (fs.existsSync(commonDirFile)) { const commonPath = fs.readFileSync(commonDirFile, "utf-8").trim(); - return path.resolve(absGitDir, commonPath); + const resolvedGitDir = realpathOrSelf(gitPath); + return realpathOrSelf(path.resolve(resolvedGitDir, commonPath)); } - // Fallback: assume standard structure - return path.resolve(absGitDir, "../../"); + return null; } catch { return null; } diff --git a/src/lib/workers/colbert-math.ts b/src/lib/workers/colbert-math.ts deleted file mode 100644 index ea5b04d9..00000000 --- a/src/lib/workers/colbert-math.ts +++ /dev/null @@ -1,92 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { inner } from "simsimd"; -import { MODEL_IDS, PATHS } from "../../config"; - -let SKIP_IDS: Set | null = null; - -function loadSkipIds(): Set { - if (SKIP_IDS) return SKIP_IDS; - - // Check local models first (same logic as orchestrator) - const PROJECT_ROOT = process.env.OSGREP_PROJECT_ROOT - ? path.resolve(process.env.OSGREP_PROJECT_ROOT) - : process.cwd(); - const localModels = path.join(PROJECT_ROOT, "models"); - const localColbert = path.join(localModels, ...MODEL_IDS.colbert.split("/")); - const localSkipPath = path.join(localColbert, "skiplist.json"); - - // Try local first, then global - const globalBasePath = path.join( - PATHS.models, - ...MODEL_IDS.colbert.split("/"), - ); - const globalSkipPath = path.join(globalBasePath, "skiplist.json"); - - const skipPath = fs.existsSync(localSkipPath) - ? localSkipPath - : globalSkipPath; - - if (fs.existsSync(skipPath)) { - try { - const parsed = JSON.parse(fs.readFileSync(skipPath, "utf8")) as number[]; - SKIP_IDS = new Set(parsed.map((n) => Number(n))); - return SKIP_IDS; - } catch (_e) { - // fall through to empty set - } - } - SKIP_IDS = new Set(); - return SKIP_IDS; -} - -export function maxSim( - queryEmbeddings: number[][] | Float32Array[], - docEmbeddings: number[][] | Float32Array[], - docTokenIds?: number[], -): number { - if (queryEmbeddings.length === 0 || docEmbeddings.length === 0) { - return 0; - } - - const qVecs = queryEmbeddings.map((v) => - v instanceof Float32Array ? v : new Float32Array(v), - ); - const dVecs = docEmbeddings.map((v) => - v instanceof Float32Array ? v : new Float32Array(v), - ); - const dTokenIds = - docTokenIds && docTokenIds.length === dVecs.length ? docTokenIds : null; - const skipIds = loadSkipIds(); - - let totalScore = 0; - for (const qVec of qVecs) { - let maxDotProduct = -Infinity; - for (let idx = 0; idx < dVecs.length; idx++) { - const tokenId = dTokenIds ? dTokenIds[idx] : null; - if (tokenId !== null && skipIds.has(Number(tokenId))) continue; - const dVec = dVecs[idx]; - const dim = Math.min(qVec.length, dVec.length); - const dot = inner(qVec.subarray(0, dim), dVec.subarray(0, dim)); - if (dot > maxDotProduct) maxDotProduct = dot; - } - if (maxDotProduct === -Infinity) maxDotProduct = 0; - totalScore += maxDotProduct; - } - - return totalScore; -} - -export function cosineSim( - a: number[] | Float32Array, - b: number[] | Float32Array, -): number { - const aVec = a instanceof Float32Array ? a : new Float32Array(a); - const bVec = b instanceof Float32Array ? b : new Float32Array(b); - - const dim = Math.min(aVec.length, bVec.length); - if (aVec.length !== bVec.length) { - return inner(aVec.subarray(0, dim), bVec.subarray(0, dim)); - } - return inner(aVec, bVec); -} diff --git a/src/lib/workers/colbert-tokenizer.ts b/src/lib/workers/colbert-tokenizer.ts deleted file mode 100644 index 0571a1e5..00000000 --- a/src/lib/workers/colbert-tokenizer.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { - AutoTokenizer, - type PreTrainedTokenizer, -} from "@huggingface/transformers"; - -const QUERY_MARKER_TOKEN = "[Q] "; -const DOC_MARKER_TOKEN = "[D] "; -const MASK_TOKEN = "[MASK]"; -const QUERY_MAXLEN = 32; // Standard ColBERT query length -const DOC_MAXLEN = 512; // Standard ColBERT document length - -export class ColBERTTokenizer { - private tokenizer: PreTrainedTokenizer | null = null; - private specialTokenIds: { - cls: number; - sep: number; - pad: number; - mask: number; - queryMarker: number; - docMarker: number; - } | null = null; - - async init(modelPath: string) { - this.tokenizer = await AutoTokenizer.from_pretrained(modelPath); - - // Get special token IDs with fallbacks - // We use the IDs we discovered in validation: [Q]=50368, [D]=50369 - // But we still try to look them up dynamically first. - - const tokenizer = this.tokenizer; - const get = (token: string) => tokenizer?.model.tokens_to_ids.get(token); - - const specialTokens = tokenizer as Partial<{ - cls_token: string; - sep_token: string; - pad_token: string; - }>; - const clsId = get(specialTokens.cls_token ?? "[CLS]") ?? 50281; - const sepId = get(specialTokens.sep_token ?? "[SEP]") ?? 50282; - const padId = get(specialTokens.pad_token ?? "[PAD]") ?? 50283; - const maskId = get(MASK_TOKEN) ?? 50284; - const queryMarkerId = get(QUERY_MARKER_TOKEN) ?? 50368; - const docMarkerId = get(DOC_MARKER_TOKEN) ?? 50369; - - this.specialTokenIds = { - cls: clsId, - sep: sepId, - pad: padId, - mask: maskId, - queryMarker: queryMarkerId, - docMarker: docMarkerId, - }; - } - - async encodeQuery( - text: string, - ): Promise<{ input_ids: bigint[]; attention_mask: bigint[] }> { - if (!this.tokenizer || !this.specialTokenIds) { - throw new Error("Tokenizer not initialized. Call init() first."); - } - - // Tokenize without special tokens - const encoded = await this.tokenizer(text, { - add_special_tokens: false, - truncation: true, - max_length: QUERY_MAXLEN - 2, // Reserve space for [CLS] and [Q] - }); - - const { input_ids } = encoded; - - // Build sequence: [CLS] [Q] token1 token2 ... [SEP] [MASK] [MASK] ... - const finalIds: number[] = [ - this.specialTokenIds.cls, - this.specialTokenIds.queryMarker, - ...Array.from(input_ids.data as BigInt64Array).map(Number), - this.specialTokenIds.sep, - ]; - - // Query Expansion: pad with [MASK] tokens up to QUERY_MAXLEN - while (finalIds.length < QUERY_MAXLEN) { - finalIds.push(this.specialTokenIds.mask); - } - - // Truncate if somehow longer (safety check) - if (finalIds.length > QUERY_MAXLEN) { - finalIds.length = QUERY_MAXLEN; - } - - // Create attention mask (1 for all tokens, since MASK is also attended to) - const attentionMask = new Array(finalIds.length).fill(1); - - return { - input_ids: finalIds.map((id) => BigInt(id)), - attention_mask: attentionMask.map((v) => BigInt(v)), - }; - } - - async encodeDoc( - text: string, - ): Promise<{ input_ids: bigint[]; attention_mask: bigint[] }> { - if (!this.tokenizer || !this.specialTokenIds) { - throw new Error("Tokenizer not initialized. Call init() first."); - } - - // Tokenize without special tokens - const encoded = await this.tokenizer(text, { - add_special_tokens: false, - truncation: true, - max_length: DOC_MAXLEN - 3, // Reserve space for [CLS], [D], and [SEP] - }); - - const { input_ids } = encoded; - - // Build sequence: [CLS] [D] token1 token2 ... [SEP] - const finalIds: number[] = [ - this.specialTokenIds.cls, - this.specialTokenIds.docMarker, - ...Array.from(input_ids.data as BigInt64Array).map(Number), - this.specialTokenIds.sep, - ]; - - // Create attention mask - const attentionMask = new Array(finalIds.length).fill(1); - - return { - input_ids: finalIds.map((id) => BigInt(id)), - attention_mask: attentionMask.map((v) => BigInt(v)), - }; - } -} diff --git a/src/lib/workers/download-worker.ts b/src/lib/workers/download-worker.ts deleted file mode 100644 index 1bfd9040..00000000 --- a/src/lib/workers/download-worker.ts +++ /dev/null @@ -1,150 +0,0 @@ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { parentPort } from "node:worker_threads"; -import { env, pipeline } from "@huggingface/transformers"; -import { MODEL_IDS } from "../../config"; - -// Configuration -const HOMEDIR = os.homedir(); -const CACHE_DIR = path.join(HOMEDIR, ".osgrep", "models"); -env.cacheDir = CACHE_DIR; -env.allowLocalModels = true; -env.allowRemoteModels = true; - -// Suppress noisy warnings from transformers.js/onnxruntime -const originalWarn = console.warn; -console.warn = (...args) => { - if ( - args[0] && - typeof args[0] === "string" && - args[0].includes("Unable to determine content-length") - ) { - return; - } - originalWarn(...args); -}; - -type QuantizationDType = - | "auto" - | "fp32" - | "fp16" - | "q8" - | "int8" - | "uint8" - | "q4" - | "bnb4" - | "q4f16"; - -type PipelineDType = QuantizationDType | Record; - -// Helper to download with timeout -async function downloadModelWithTimeout(modelId: string, dtype: PipelineDType) { - const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes - - try { - const downloadPromise = pipeline("feature-extraction", modelId, { - dtype, - progress_callback: (progress: unknown) => { - if (parentPort) parentPort.postMessage({ type: "progress", progress }); - }, - }); - - const timeoutPromise = new Promise((_, reject) => { - setTimeout( - () => reject(new Error(`Download timed out after ${TIMEOUT_MS} ms`)), - TIMEOUT_MS, - ); - }); - - return Promise.race([downloadPromise, timeoutPromise]); - } catch (err) { - console.error(`Worker: pipeline creation failed for ${modelId}: `, err); - throw err; - } -} - -// Helper to manually download extra files like skiplist.json -async function downloadExtraFile(modelId: string, filename: string) { - const url = `https://huggingface.co/${modelId}/resolve/main/${filename}`; - // Construct path: ~/.osgrep/models/ryandono/osgrep-colbert-q8/skiplist.json - const destDir = path.join(CACHE_DIR, ...modelId.split("/")); - const destPath = path.join(destDir, filename); - - if (!fs.existsSync(destDir)) { - fs.mkdirSync(destDir, { recursive: true }); - } - - // If file exists and is non-zero, skip (or implement hash check if you want SOTA robustness) - if (fs.existsSync(destPath) && fs.statSync(destPath).size > 0) { - return; - } - - if (parentPort) { - parentPort.postMessage({ - type: "progress", - progress: { status: "downloading", file: filename }, - }); - } - - try { - const res = await fetch(url); - if (!res.ok) { - throw new Error(`HTTP ${res.status}: ${res.statusText}`); - } - const buffer = await res.arrayBuffer(); - fs.writeFileSync(destPath, Buffer.from(buffer)); - if (parentPort) { - parentPort.postMessage({ - type: "progress", - progress: { status: "downloaded", file: filename }, - }); - } - } catch (e) { - const errorMsg = e instanceof Error ? e.message : String(e); - console.warn(`āš ļø Failed to download ${filename} from ${url}:`, errorMsg); - // Don't crash, just warn. The math worker has a fallback (empty set). - // But report the failure so setup can retry - if (parentPort) { - parentPort.postMessage({ - type: "warning", - file: filename, - error: errorMsg, - }); - } - } -} - -async function download() { - try { - // 1. Download Dense Model - const embedPipeline = await downloadModelWithTimeout(MODEL_IDS.embed, "q4"); - await embedPipeline.dispose(); - - // 2. Download ColBERT Model - const colbertPipeline = await downloadModelWithTimeout( - MODEL_IDS.colbert, - "int8", - ); - await colbertPipeline.dispose(); - - // 3. Download the custom Skiplist - await downloadExtraFile(MODEL_IDS.colbert, "skiplist.json"); - - if (parentPort) { - parentPort.postMessage({ status: "success" }); - } else { - process.exit(0); - } - } catch (error) { - console.error("Worker failed to download models:", error); - if (parentPort) { - const errorMsg = error instanceof Error ? error.message : String(error); - parentPort.postMessage({ status: "error", error: errorMsg }); - } else { - process.exit(1); - } - } -} - -download(); diff --git a/src/lib/workers/embeddings/colbert.ts b/src/lib/workers/embeddings/colbert.ts deleted file mode 100644 index 0982db18..00000000 --- a/src/lib/workers/embeddings/colbert.ts +++ /dev/null @@ -1,199 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import * as ort from "onnxruntime-node"; -import { MODEL_IDS, PATHS } from "../../../config"; -import { ColBERTTokenizer } from "../colbert-tokenizer"; - -const CACHE_DIR = PATHS.models; -const ONNX_THREADS = 1; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -export type HybridResult = { - dense: Float32Array; - colbert: Int8Array; - scale: number; - pooled_colbert_48d?: Float32Array; - token_ids?: number[]; -}; - -export class ColbertModel { - private session: ort.InferenceSession | null = null; - public tokenizer: ColBERTTokenizer | null = null; - - async load() { - if (this.session && this.tokenizer) return; - - this.tokenizer = new ColBERTTokenizer(); - - const basePath = path.join(CACHE_DIR, MODEL_IDS.colbert); - const onnxDir = path.join(basePath, "onnx"); - - const modelPath = path.join(onnxDir, "model_int8.onnx"); - if (!fs.existsSync(modelPath)) { - throw new Error(`ColBERT ONNX model not found at ${modelPath}`); - } - - await this.tokenizer.init(basePath); - - const sessionOptions: ort.InferenceSession.SessionOptions = { - executionProviders: ["cpu"], - intraOpNumThreads: ONNX_THREADS, - interOpNumThreads: 1, - graphOptimizationLevel: "all", - }; - - log(`Worker: Loading ColBERT ONNX session from ${modelPath}`); - this.session = await ort.InferenceSession.create(modelPath, sessionOptions); - - if (!this.session) { - throw new Error(`ColBERT ONNX load failed; tried ${modelPath}`); - } - } - - isReady(): boolean { - return !!(this.session && this.tokenizer); - } - - async runBatch( - texts: string[], - denseVectors: Float32Array[], - vectorDimensions: number, - ): Promise { - if (!this.session || !this.tokenizer) return []; - const tokenizer = this.tokenizer; - const session = this.session; - - const encodedBatch = await Promise.all( - texts.map((t) => tokenizer.encodeDoc(t)), - ); - - const maxLen = Math.max(...encodedBatch.map((e) => e.input_ids.length)); - const batchInputIds = new BigInt64Array(texts.length * maxLen); - const batchAttentionMask = new BigInt64Array(texts.length * maxLen); - const padId = BigInt(50283); - - for (let i = 0; i < encodedBatch.length; i++) { - const encoded = encodedBatch[i]; - const offset = i * maxLen; - for (let j = 0; j < maxLen; j++) { - if (j < encoded.input_ids.length) { - batchInputIds[offset + j] = encoded.input_ids[j]; - batchAttentionMask[offset + j] = encoded.attention_mask[j]; - } else { - batchInputIds[offset + j] = padId; - batchAttentionMask[offset + j] = BigInt(0); - } - } - } - - const feeds = { - input_ids: new ort.Tensor("int64", batchInputIds, [texts.length, maxLen]), - attention_mask: new ort.Tensor("int64", batchAttentionMask, [ - texts.length, - maxLen, - ]), - }; - - const sessionOut = await session.run(feeds); - const outputName = session.outputNames[0]; - const output = sessionOut[outputName]; - if (!output) { - throw new Error("ColBERT session output missing embeddings tensor"); - } - - const data = output.data as Float32Array; - const [batch, seq, dim] = output.dims as number[]; - const results: HybridResult[] = []; - - for (let b = 0; b < batch; b++) { - const batchOffset = b * seq * dim; - const originalLen = encodedBatch[b].input_ids.length; - const normalized = new Float32Array(originalLen * dim); - let maxVal = 0; - - for (let s = 0; s < originalLen; s++) { - const offset = batchOffset + s * dim; - let sumSq = 0; - for (let d = 0; d < dim; d++) { - const val = data[offset + d]; - sumSq += val * val; - } - const norm = Math.sqrt(sumSq) || 1; - - for (let d = 0; d < dim; d++) { - const val = data[offset + d] / norm; - const idx = s * dim + d; - normalized[idx] = val; - if (Math.abs(val) > maxVal) maxVal = Math.abs(val); - } - } - - if (maxVal === 0) maxVal = 1; - - const int8Array = new Int8Array(normalized.length); - for (let i = 0; i < normalized.length; i++) { - int8Array[i] = Math.max( - -127, - Math.min(127, Math.round((normalized[i] / maxVal) * 127)), - ); - } - - const pooled = new Float32Array(dim); - const tokenCount = Math.max(1, originalLen); - for (let s = 0; s < originalLen; s++) { - const tokenOffset = s * dim; - for (let d = 0; d < dim; d++) { - pooled[d] += normalized[tokenOffset + d]; - } - } - let pooledNorm = 0; - for (let d = 0; d < dim; d++) { - pooled[d] /= tokenCount; - pooledNorm += pooled[d] * pooled[d]; - } - pooledNorm = Math.sqrt(pooledNorm) || 1; - for (let d = 0; d < dim; d++) { - pooled[d] /= pooledNorm; - } - - results.push({ - dense: denseVectors[b] ?? new Float32Array(vectorDimensions).fill(0), - colbert: int8Array, - scale: maxVal, - pooled_colbert_48d: pooled, - token_ids: Array.from(encodedBatch[b].input_ids, (v) => Number(v)), - }); - } - - return results; - } - - async encodeQuery(text: string): Promise<{ - input_ids: BigInt64Array; - attention_mask: BigInt64Array; - }> { - if (!this.tokenizer) throw new Error("ColBERT tokenizer not initialized"); - const encoded = await this.tokenizer.encodeQuery(text); - return { - input_ids: new BigInt64Array(encoded.input_ids), - attention_mask: new BigInt64Array(encoded.attention_mask), - }; - } - - async runSession( - feeds: Record, - ): Promise { - if (!this.session) throw new Error("ColBERT session not initialized"); - return this.session.run(feeds); - } - - getOutputName(): string { - if (!this.session) throw new Error("ColBERT session not initialized"); - return this.session.outputNames[0]; - } -} diff --git a/src/lib/workers/embeddings/granite.ts b/src/lib/workers/embeddings/granite.ts deleted file mode 100644 index b468061f..00000000 --- a/src/lib/workers/embeddings/granite.ts +++ /dev/null @@ -1,175 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { - AutoTokenizer, - type PreTrainedTokenizer, -} from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; -import { CONFIG, MODEL_IDS, PATHS } from "../../../config"; - -const CACHE_DIR = PATHS.models; -const ONNX_THREADS = 1; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -export class GraniteModel { - private session: ort.InferenceSession | null = null; - private tokenizer: PreTrainedTokenizer | null = null; - private readonly vectorDimensions = CONFIG.VECTOR_DIM; - - private resolvePaths(): { modelPath: string; tokenizerPath: string } { - const basePath = path.join(CACHE_DIR, MODEL_IDS.embed); - const onnxDir = path.join(basePath, "onnx"); - const candidates = ["model_q4.onnx", "model.onnx"]; - - for (const candidate of candidates) { - const candidatePath = path.join(onnxDir, candidate); - if (fs.existsSync(candidatePath)) { - return { modelPath: candidatePath, tokenizerPath: basePath }; - } - } - - throw new Error( - `Granite ONNX model not found. Looked for ${candidates.join( - ", ", - )} in ${onnxDir}`, - ); - } - - async load() { - if (this.session && this.tokenizer) return; - - const { modelPath, tokenizerPath } = this.resolvePaths(); - log(`Worker: Loading Granite ONNX session from ${modelPath}`); - - this.tokenizer = await AutoTokenizer.from_pretrained(tokenizerPath); - - const sessionOptions: ort.InferenceSession.SessionOptions = { - executionProviders: ["cpu"], - intraOpNumThreads: ONNX_THREADS, - interOpNumThreads: 1, - graphOptimizationLevel: "all", - }; - this.session = await ort.InferenceSession.create(modelPath, sessionOptions); - } - - isReady(): boolean { - return !!(this.session && this.tokenizer); - } - - private meanPool( - hidden: Float32Array, - attention: BigInt64Array, - batch: number, - seq: number, - hiddenDim: number, - targetDim: number, - ): Float32Array[] { - const vectors: Float32Array[] = []; - const seqFromMask = attention.length / Math.max(1, batch); - const usableSeq = Math.min(seq, seqFromMask); - const dim = Math.min(hiddenDim, targetDim); - - for (let b = 0; b < batch; b++) { - const sum = new Float32Array(dim); - let count = 0; - const attOffset = b * seqFromMask; - const hiddenOffset = b * seq * hiddenDim; - - for (let s = 0; s < usableSeq; s++) { - if (attention[attOffset + s] > 0) { - count++; - const tokenOffset = hiddenOffset + s * hiddenDim; - for (let d = 0; d < dim; d++) { - sum[d] += hidden[tokenOffset + d]; - } - } - } - - if (count === 0) count = 1; - let norm = 0; - for (let d = 0; d < dim; d++) { - sum[d] /= count; - norm += sum[d] * sum[d]; - } - norm = Math.sqrt(norm) || 1; - for (let d = 0; d < dim; d++) { - sum[d] /= norm; - } - - if (dim < targetDim) { - const padded = new Float32Array(targetDim); - padded.set(sum); - vectors.push(padded); - } else { - vectors.push(sum); - } - } - - return vectors; - } - - async runBatch(texts: string[]): Promise { - if (!this.session || !this.tokenizer) return []; - - const encoded = await this.tokenizer(texts, { - padding: true, - truncation: true, - max_length: 256, - }); - - type EncodedTensor = { data: BigInt64Array; dims?: number[] }; - const inputTensor = encoded.input_ids as unknown as EncodedTensor; - const attentionTensor = encoded.attention_mask as unknown as EncodedTensor; - const inputIds = inputTensor.data; - const attentionMask = attentionTensor.data; - const seqLen = - inputTensor.dims?.[1] ?? - Math.max(1, Math.floor(inputIds.length / texts.length)); - - const tokenTypeIdsRaw = ( - encoded as Partial<{ token_type_ids: EncodedTensor }> - ).token_type_ids; - const tokenTypeIds = - tokenTypeIdsRaw && - tokenTypeIdsRaw.data.length === inputIds.length && - tokenTypeIdsRaw.data.length === attentionMask.length - ? tokenTypeIdsRaw.data - : new BigInt64Array(inputIds.length).fill(BigInt(0)); - - const feeds = { - input_ids: new ort.Tensor("int64", inputIds, [texts.length, seqLen]), - attention_mask: new ort.Tensor("int64", attentionMask, [ - texts.length, - seqLen, - ]), - token_type_ids: new ort.Tensor("int64", tokenTypeIds, [ - texts.length, - seqLen, - ]), - }; - - const sessionOut = await this.session.run(feeds); - const hidden = - sessionOut.last_hidden_state ?? sessionOut[this.session.outputNames[0]]; - - if (!hidden) { - throw new Error("Granite ONNX output missing last_hidden_state"); - } - - const hiddenData = hidden.data as Float32Array; - const [batch, seq, dim] = hidden.dims as number[]; - return this.meanPool( - hiddenData, - attentionMask, - batch, - seq, - dim, - this.vectorDimensions, - ); - } -} diff --git a/src/lib/workers/orchestrator.ts b/src/lib/workers/orchestrator.ts index 63a366dd..6c8ec841 100644 --- a/src/lib/workers/orchestrator.ts +++ b/src/lib/workers/orchestrator.ts @@ -1,16 +1,24 @@ -import * as fs from "node:fs"; +/** + * Orchestrator: Coordinates file processing and embedding + * + * This module handles: + * - File reading and validation + * - Chunking via tree-sitter + * - Embedding via native Rust code + * + * No worker pool needed - Rust ONNX Runtime is fast and stable. + */ + +import * as crypto from "node:crypto"; import * as path from "node:path"; -import { env } from "@huggingface/transformers"; -import * as ort from "onnxruntime-node"; -import { v4 as uuidv4 } from "uuid"; -import { CONFIG, PATHS } from "../../config"; +import { CONFIG } from "../../config"; import { buildAnchorChunk, type ChunkWithContext, formatChunkText, TreeSitterChunker, } from "../index/chunker"; -import { Skeletonizer } from "../skeleton"; +import { embedBatch, initNative, encodeQueryColbert, rerankColbert } from "../native"; import type { PreparedChunk, VectorRecord } from "../store/types"; import { computeBufferHash, @@ -18,9 +26,10 @@ import { isIndexableFile, readFileSnapshot, } from "../utils/file-utils"; -import { maxSim } from "./colbert-math"; -import { ColbertModel, type HybridResult } from "./embeddings/colbert"; -import { GraniteModel } from "./embeddings/granite"; + +// ============================================================================= +// Types +// ============================================================================= export type ProcessFileInput = { path: string; @@ -37,52 +46,26 @@ export type ProcessFileResult = { export type RerankDoc = { colbert: Buffer | Int8Array | number[]; - scale: number; token_ids?: number[]; }; -const CACHE_DIR = PATHS.models; -const LOG_MODELS = - process.env.OSGREP_DEBUG_MODELS === "1" || - process.env.OSGREP_DEBUG_MODELS === "true"; -const log = (...args: unknown[]) => { - if (LOG_MODELS) console.log(...args); -}; - -env.cacheDir = CACHE_DIR; -env.allowLocalModels = true; -env.allowRemoteModels = true; +// ============================================================================= +// Orchestrator +// ============================================================================= const PROJECT_ROOT = process.env.OSGREP_PROJECT_ROOT ? path.resolve(process.env.OSGREP_PROJECT_ROOT) : process.cwd(); -const LOCAL_MODELS = path.join(PROJECT_ROOT, "models"); -if (fs.existsSync(LOCAL_MODELS)) { - env.localModelPath = LOCAL_MODELS; - log(`Worker: Using local models from ${LOCAL_MODELS}`); -} export class WorkerOrchestrator { - private granite = new GraniteModel(); - private colbert = new ColbertModel(); private chunker = new TreeSitterChunker(); - private skeletonizer = new Skeletonizer(); private initPromise: Promise | null = null; - private readonly vectorDimensions = CONFIG.VECTOR_DIM; private async ensureReady() { - if (this.granite.isReady() && this.colbert.isReady()) { - return; - } if (this.initPromise) return this.initPromise; this.initPromise = (async () => { - await Promise.all([ - this.chunker.init(), - this.skeletonizer.init(), - this.granite.load(), - this.colbert.load(), - ]); + await Promise.all([this.chunker.init(), initNative()]); })().finally(() => { this.initPromise = null; }); @@ -90,46 +73,14 @@ export class WorkerOrchestrator { return this.initPromise; } - private async computeHybrid( - texts: string[], - onProgress?: () => void, - ): Promise { - if (!texts.length) return []; - await this.ensureReady(); - - const results: HybridResult[] = []; - const envBatch = Number.parseInt( - process.env.OSGREP_WORKER_BATCH_SIZE ?? "", - 10, - ); - const BATCH_SIZE = - Number.isFinite(envBatch) && envBatch > 0 - ? Math.max(4, Math.min(16, envBatch)) - : 16; - for (let i = 0; i < texts.length; i += BATCH_SIZE) { - if (i > 0) onProgress?.(); - const batchTexts = texts.slice(i, i + BATCH_SIZE); - const denseBatch = await this.granite.runBatch(batchTexts); - const colbertBatch = await this.colbert.runBatch( - batchTexts, - denseBatch, - this.vectorDimensions, - ); - results.push(...colbertBatch); - } - onProgress?.(); - - return results; - } - private async chunkFile( pathname: string, - content: string, + content: string ): Promise { await this.ensureReady(); const { chunks: parsedChunks, metadata } = await this.chunker.chunk( pathname, - content, + content ); const anchorChunk = buildAnchorChunk(pathname, content, metadata); @@ -159,12 +110,11 @@ export class WorkerOrchestrator { } private toPreparedChunks( - path: string, + filePath: string, hash: string, - chunks: ChunkWithContext[], - skeleton?: string, + chunks: ChunkWithContext[] ): PreparedChunk[] { - const texts = chunks.map((chunk) => formatChunkText(chunk, path)); + const texts = chunks.map((chunk) => formatChunkText(chunk, filePath)); const prepared: PreparedChunk[] = []; for (let i = 0; i < texts.length; i++) { @@ -174,11 +124,11 @@ export class WorkerOrchestrator { const next = texts[i + 1]?.displayText; prepared.push({ - id: uuidv4(), - path, + id: crypto.randomUUID(), + path: filePath, hash, - content: content, // Now minimal - display_text: displayText, // Now rich + content, + display_text: displayText, context_prev: typeof prev === "string" ? prev : undefined, context_next: typeof next === "string" ? next : undefined, start_line: chunk.startLine, @@ -192,7 +142,6 @@ export class WorkerOrchestrator { referenced_symbols: chunk.referencedSymbols, role: chunk.role, parent_symbol: chunk.parentSymbol, - file_skeleton: chunk.isAnchor ? skeleton : undefined, }); } @@ -201,7 +150,7 @@ export class WorkerOrchestrator { async processFile( input: ProcessFileInput, - onProgress?: () => void, + onProgress?: () => void ): Promise { const absolutePath = path.isAbsolute(input.path) ? input.path @@ -225,49 +174,26 @@ export class WorkerOrchestrator { onProgress?.(); const content = buffer.toString("utf-8"); - const chunksPromise = this.chunkFile(input.path, content); - - // Generate skeleton in parallel - const skeletonPromise = this.skeletonizer.skeletonizeFile( - input.path, - content, - { - includeSummary: true, - }, - ); - - const [chunks, skeletonResult] = await Promise.all([ - chunksPromise, - skeletonPromise, - ]); + const chunks = await this.chunkFile(input.path, content); onProgress?.(); if (!chunks.length) return { vectors: [], hash, mtimeMs, size }; - const preparedChunks = this.toPreparedChunks( - input.path, - hash, - chunks, - skeletonResult.success ? skeletonResult.skeleton : undefined, - ); - const hybrids = await this.computeHybrid( - preparedChunks.map((chunk) => chunk.content), - onProgress, + const preparedChunks = this.toPreparedChunks(input.path, hash, chunks); + + // Embed all chunks via native Rust + const hybrids = await embedBatch( + preparedChunks.map((chunk) => chunk.content) ); + onProgress?.(); - const vectors = preparedChunks.map((chunk, idx) => { - const hybrid = hybrids[idx] ?? { - dense: new Float32Array(), - colbert: new Int8Array(), - scale: 1, - }; + const vectors: VectorRecord[] = preparedChunks.map((chunk, idx) => { + const hybrid = hybrids[idx]; return { ...chunk, vector: hybrid.dense, colbert: Buffer.from(hybrid.colbert), - colbert_scale: hybrid.scale, - pooled_colbert_48d: hybrid.pooled_colbert_48d, - doc_token_ids: hybrid.token_ids, + doc_token_ids: Array.from(hybrid.token_ids), }; }); @@ -279,84 +205,32 @@ export class WorkerOrchestrator { dense: number[]; colbert: number[][]; colbertDim: number; - pooled_colbert_48d?: number[]; }> { await this.ensureReady(); - const [denseVector] = await this.granite.runBatch([text]); + // Get dense embedding + const hybrids = await embedBatch([text]); + const denseVector = hybrids[0].dense; - const encoded = await this.colbert.encodeQuery(text); - - const feeds = { - input_ids: new ort.Tensor("int64", encoded.input_ids, [ - 1, - encoded.input_ids.length, - ]), - attention_mask: new ort.Tensor("int64", encoded.attention_mask, [ - 1, - encoded.attention_mask.length, - ]), - }; - - const sessionOut = await this.colbert.runSession(feeds); - const outputName = this.colbert.getOutputName(); - const output = sessionOut[outputName]; - if (!output) { - throw new Error("ColBERT session output missing embeddings tensor"); - } - - const data = output.data as Float32Array; - const [, seq, dim] = output.dims as number[]; + // Get ColBERT query embedding + const colbertFlat = await encodeQueryColbert(text); + const dim = CONFIG.COLBERT_DIM; + const seqLen = colbertFlat.length / dim; + // Reshape to matrix const matrix: number[][] = []; - - for (let s = 0; s < seq; s++) { - let sumSq = 0; - const offset = s * dim; - for (let d = 0; d < dim; d++) { - const val = data[offset + d]; - sumSq += val * val; - } - const norm = Math.sqrt(sumSq); - + for (let s = 0; s < seqLen; s++) { const row: number[] = []; - if (norm > 1e-9) { - for (let d = 0; d < dim; d++) { - row.push(data[offset + d] / norm); - } - } else { - for (let d = 0; d < dim; d++) { - row.push(data[offset + d]); - } - } - matrix.push(row); - } - - // Compute pooled embedding (mean of tokens) - const pooled = new Float32Array(dim); - for (const row of matrix) { - for (let d = 0; d < dim; d++) { - pooled[d] += row[d]; - } - } - // Normalize pooled - let sumSq = 0; - for (let d = 0; d < dim; d++) { - pooled[d] /= matrix.length || 1; - sumSq += pooled[d] * pooled[d]; - } - const norm = Math.sqrt(sumSq); - if (norm > 1e-9) { for (let d = 0; d < dim; d++) { - pooled[d] /= norm; + row.push(colbertFlat[s * dim + d]); } + matrix.push(row); } return { - dense: Array.from(denseVector ?? []), + dense: Array.from(denseVector), colbert: matrix, colbertDim: dim, - pooled_colbert_48d: Array.from(pooled), }; } @@ -366,27 +240,37 @@ export class WorkerOrchestrator { colbertDim: number; }): Promise { await this.ensureReady(); - const queryMatrix = input.query.map((row) => - row instanceof Float32Array ? row : new Float32Array(row), - ); - return input.docs.map((doc) => { + // Flatten query matrix to match native `rerankColbert` signature + const queryEmbedding: number[] = []; + for (const row of input.query) { + for (let i = 0; i < row.length; i++) { + queryEmbedding.push(row[i] ?? 0); + } + } + + const docLengths: number[] = []; + const docOffsets: number[] = []; + const candidateIndices: number[] = []; + const packedTokenChunks: Uint32Array[] = []; + + // Pack all doc embeddings into a single buffer; offsets are element offsets + const packedChunks: Int8Array[] = []; + let totalElements = 0; + let totalTokenIds = 0; + + for (let i = 0; i < input.docs.length; i++) { + const doc = input.docs[i]; const col = doc.colbert; - let colbert: Int8Array; + let colbert: Int8Array; if (col instanceof Int8Array) { colbert = col; } else if (Buffer.isBuffer(col)) { colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength); - } else if ( - col && - typeof col === "object" && - "type" in col && - (col as any).type === "Buffer" && - Array.isArray((col as any).data) - ) { - // IPC serialization fallback (still copies, but unavoidable without SharedArrayBuffer) - colbert = new Int8Array((col as any).data); + } else if (ArrayBuffer.isView(col)) { + // Handles Uint8Array and other typed arrays (e.g. from LanceDB) + colbert = new Int8Array(col.buffer, col.byteOffset, col.byteLength); } else if (Array.isArray(col)) { colbert = new Int8Array(col); } else { @@ -394,20 +278,85 @@ export class WorkerOrchestrator { } const seqLen = Math.floor(colbert.length / input.colbertDim); - const docMatrix: Float32Array[] = []; - for (let i = 0; i < seqLen; i++) { - const start = i * input.colbertDim; - const row = new Float32Array(input.colbertDim); - for (let d = 0; d < input.colbertDim; d++) { - row[d] = (colbert[start + d] * doc.scale) / 127.0; - } - docMatrix.push(row); - } - const tokenIds = - Array.isArray(doc.token_ids) && doc.token_ids.length === seqLen - ? doc.token_ids - : undefined; - return maxSim(queryMatrix, docMatrix, tokenIds); + const used = colbert.subarray(0, seqLen * input.colbertDim); + + const tokenIdsRaw = doc.token_ids ?? []; + const tokenIds = Uint32Array.from( + tokenIdsRaw.slice(0, seqLen).map((v) => (Number.isFinite(v) ? v : 0)), + ); + + docOffsets.push(totalElements); + docLengths.push(seqLen); + candidateIndices.push(i); + packedChunks.push(used); + packedTokenChunks.push(tokenIds); + totalElements += used.length; + totalTokenIds += tokenIds.length; + } + + const packed = new Int8Array(totalElements); + let cursor = 0; + for (const chunk of packedChunks) { + packed.set(chunk, cursor); + cursor += chunk.length; + } + + const packedTokenIds = new Uint32Array(totalTokenIds); + let tokenCursor = 0; + for (const chunk of packedTokenChunks) { + packedTokenIds.set(chunk, tokenCursor); + tokenCursor += chunk.length; + } + + const result = await rerankColbert({ + queryEmbedding: new Float32Array(queryEmbedding), + docEmbeddings: packed, + docTokenIds: packedTokenIds, + docLengths, + docOffsets, + candidateIndices, + topK: input.docs.length, }); + + const scoreByIndex = new Map(); + for (let i = 0; i < result.indices.length; i++) { + const idx = result.indices[i] ?? -1; + const score = result.scores[i] ?? 0; + if (typeof idx === "number") scoreByIndex.set(idx, score); + } + + // Return scores aligned to input order (Searcher expects this) + return candidateIndices.map((i) => scoreByIndex.get(i) ?? 0); + } +} + +// ============================================================================= +// Singleton for direct use (no worker pool needed) +// ============================================================================= + +let orchestrator: WorkerOrchestrator | null = null; + +export function getOrchestrator(): WorkerOrchestrator { + if (!orchestrator) { + orchestrator = new WorkerOrchestrator(); } + return orchestrator; +} + +export async function processFile( + input: ProcessFileInput +): Promise { + return getOrchestrator().processFile(input); +} + +export async function encodeQuery(text: string) { + return getOrchestrator().encodeQuery(text); +} + +export async function rerank(input: { + query: number[][]; + docs: RerankDoc[]; + colbertDim: number; +}) { + return getOrchestrator().rerank(input); } diff --git a/src/lib/workers/pool.ts b/src/lib/workers/pool.ts deleted file mode 100644 index 0216464b..00000000 --- a/src/lib/workers/pool.ts +++ /dev/null @@ -1,450 +0,0 @@ -/** - * Architecture Note: We use a custom Child Process pool instead of Worker Threads - * to ensure the ONNX Runtime segfaults do not crash the main process. - */ -import * as childProcess from "node:child_process"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { CONFIG, WORKER_TIMEOUT_MS } from "../../config"; -import type { ProcessFileInput, ProcessFileResult, RerankDoc } from "./worker"; - -type TaskMethod = "processFile" | "encodeQuery" | "rerank"; - -type EncodeQueryResult = Awaited< - ReturnType ->; -type RerankResult = Awaited>; - -type TaskPayloads = { - processFile: ProcessFileInput; - encodeQuery: { text: string }; - rerank: { query: number[][]; docs: RerankDoc[]; colbertDim: number }; -}; - -type TaskResults = { - processFile: ProcessFileResult; - encodeQuery: EncodeQueryResult; - rerank: RerankResult; -}; - -type WorkerMessage = - | { id: number; result: TaskResults[TaskMethod] } - | { id: number; error: string } - | { id: number; heartbeat: true }; - -function reviveBufferLike(input: unknown): Buffer | Int8Array | unknown { - if ( - input && - typeof input === "object" && - "type" in (input as Record) && - (input as Record).type === "Buffer" && - Array.isArray((input as Record).data) - ) { - return Buffer.from((input as Record).data as number[]); - } - return input; -} - -function reviveProcessFileResult( - result: TaskResults["processFile"], -): TaskResults["processFile"] { - if (!result || !Array.isArray(result.vectors)) return result; - const vectors = result.vectors.map((v) => { - const revived = reviveBufferLike(v.colbert); - return revived && (Buffer.isBuffer(revived) || revived instanceof Int8Array) - ? { ...v, colbert: revived } - : v; - }); - return { ...result, vectors }; -} - -type PendingTask = { - id: number; - method: M; - payload: TaskPayloads[M]; - resolve: (value: TaskResults[M]) => void; - reject: (reason?: unknown) => void; - worker?: ProcessWorker; - timeout?: NodeJS.Timeout; -}; - -const TASK_TIMEOUT_MS = (() => { - const fromEnv = Number.parseInt( - process.env.OSGREP_WORKER_TASK_TIMEOUT_MS ?? "", - 10, - ); - if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv; - return 120_000; -})(); - -const FORCE_KILL_GRACE_MS = 200; - -class ProcessWorker { - child: childProcess.ChildProcess; - busy = false; - pendingTaskId: number | null = null; - - constructor( - public modulePath: string, - public execArgv: string[], - ) { - this.child = childProcess.fork(modulePath, { - execArgv, - env: { ...process.env }, - }); - } -} - -function resolveProcessWorker(): { filename: string; execArgv: string[] } { - const jsWorker = path.join(__dirname, "process-child.js"); - const tsWorker = path.join(__dirname, "process-child.ts"); - - if (fs.existsSync(jsWorker)) { - return { filename: jsWorker, execArgv: [] }; - } - - if (fs.existsSync(tsWorker)) { - return { filename: tsWorker, execArgv: ["-r", "ts-node/register"] }; - } - - throw new Error("Process worker file not found"); -} - -export class WorkerPool { - private workers: ProcessWorker[] = []; - private taskQueue: number[] = []; - private tasks = new Map>(); - private nextId = 1; - private destroyed = false; - private destroyPromise: Promise | null = null; - private readonly modulePath: string; - private readonly execArgv: string[]; - - constructor() { - const resolved = resolveProcessWorker(); - this.modulePath = resolved.filename; - this.execArgv = resolved.execArgv; - - const workerCount = Math.max(1, CONFIG.WORKER_THREADS); - for (let i = 0; i < workerCount; i++) { - this.spawnWorker(); - } - } - - private clearTaskTimeout(task: PendingTask) { - if (task.timeout) { - clearTimeout(task.timeout); - task.timeout = undefined; - } - } - - private removeFromQueue(taskId: number) { - const idx = this.taskQueue.indexOf(taskId); - if (idx !== -1) this.taskQueue.splice(idx, 1); - } - - private completeTask( - task: PendingTask, - worker: ProcessWorker | null, - ) { - this.clearTaskTimeout(task); - this.tasks.delete(task.id); - this.removeFromQueue(task.id); - - if (worker) { - worker.busy = false; - worker.pendingTaskId = null; - } - } - - private handleWorkerExit( - worker: ProcessWorker, - code: number | null, - signal: NodeJS.Signals | null, - ) { - worker.busy = false; - const failedTasks = Array.from(this.tasks.values()).filter( - (t) => t.worker === worker, - ); - for (const task of failedTasks) { - this.clearTaskTimeout(task); - task.reject( - new Error( - `Worker exited unexpectedly${code ? ` (code ${code})` : ""}${ - signal ? ` signal ${signal}` : "" - }`, - ), - ); - this.completeTask(task, null); - } - - this.workers = this.workers.filter((w) => w !== worker); - if (!this.destroyed) { - this.spawnWorker(); - this.dispatch(); - } - } - - private spawnWorker() { - const worker = new ProcessWorker(this.modulePath, this.execArgv); - - const onMessage = (msg: WorkerMessage) => { - const task = this.tasks.get(msg.id); - if (!task) return; - - if ("heartbeat" in msg) { - // Reset timeout - this.clearTaskTimeout(task); - if (task.worker) { - task.timeout = setTimeout( - () => this.handleTaskTimeout(task, task.worker!), - TASK_TIMEOUT_MS, - ); - } - return; - } - - if ("error" in msg) { - task.reject(new Error(msg.error)); - } else { - let result = msg.result as TaskResults[TaskMethod]; - if (task.method === "processFile") { - result = reviveProcessFileResult( - result as TaskResults["processFile"], - ) as TaskResults[TaskMethod]; - } - task.resolve(result); - } - - this.completeTask(task, worker); - this.dispatch(); - }; - - const onExit = (code: number | null, signal: NodeJS.Signals | null) => - this.handleWorkerExit(worker, code, signal); - - worker.child.on("message", onMessage); - worker.child.on("exit", onExit); - this.workers.push(worker); - } - - private enqueue( - method: M, - payload: TaskPayloads[M], - signal?: AbortSignal, - ): Promise { - if (this.destroyed) { - return Promise.reject(new Error("Worker pool destroyed")); - } - if (signal?.aborted) { - const err = new Error("Aborted"); - err.name = "AbortError"; - return Promise.reject(err); - } - - const id = this.nextId++; - return new Promise((resolve, reject) => { - let settled = false; - const safeResolve = (val: TaskResults[M]) => { - if (!settled) { - settled = true; - resolve(val); - } - }; - const safeReject = (reason?: unknown) => { - if (!settled) { - settled = true; - reject(reason); - } - }; - - const task: PendingTask = { - id, - method, - payload, - resolve: safeResolve, - reject: safeReject, - }; - - if (signal) { - signal.addEventListener( - "abort", - () => { - // If task is still in queue, remove it - const idx = this.taskQueue.indexOf(id); - if (idx !== -1) { - this.taskQueue.splice(idx, 1); - this.tasks.delete(id); - const err = new Error("Aborted"); - err.name = "AbortError"; - safeReject(err); - } - // If task is already running (assigned to worker), we can't easily kill it without - // killing the worker. For now, we just let it finish but reject the promise early so - // the caller doesn't wait. The worker will eventually finish and we'll ignore the result. - else if (this.tasks.has(id)) { - // Task is running. Reject caller immediately. - const err = new Error("Aborted"); - err.name = "AbortError"; - safeReject(err); - // We intentionally do NOT delete the task map entry here, - // because we need handleWorkerMessage to cleanly cleanup the worker state - // when it eventually finishes. - } - }, - { once: true }, - ); - } - - this.tasks.set(id, task as unknown as PendingTask); - this.taskQueue.push(id); - this.dispatch(); - }); - } - - private handleTaskTimeout( - task: PendingTask, - worker: ProcessWorker, - ) { - if (this.destroyed || !this.tasks.has(task.id)) return; - - this.clearTaskTimeout(task); - if (task.method !== "processFile") { - console.warn( - `[worker-pool] ${task.method} timed out after ${TASK_TIMEOUT_MS}ms; restarting worker.`, - ); - } - this.completeTask(task, null); - task.reject( - new Error( - `Worker task ${task.method} timed out after ${TASK_TIMEOUT_MS}ms`, - ), - ); - - worker.child.removeAllListeners("message"); - worker.child.removeAllListeners("exit"); - try { - worker.child.kill("SIGKILL"); - } catch {} - - this.workers = this.workers.filter((w) => w !== worker); - if (!this.destroyed) { - this.spawnWorker(); - } - this.dispatch(); - } - - private dispatch() { - if (this.destroyed) return; - const idle = this.workers.find((w) => !w.busy); - const nextTaskId = this.taskQueue.find((id) => { - const t = this.tasks.get(id); - return t && !t.worker; - }); - - if (!idle || nextTaskId === undefined) return; - const task = this.tasks.get(nextTaskId); - if (!task) { - this.removeFromQueue(nextTaskId); - this.dispatch(); - return; - } - - idle.busy = true; - idle.pendingTaskId = task.id; - task.worker = idle; - - task.timeout = setTimeout( - () => this.handleTaskTimeout(task, idle), - TASK_TIMEOUT_MS, - ); - - try { - idle.child.send({ - id: task.id, - method: task.method, - payload: task.payload, - }); - } catch (err) { - this.clearTaskTimeout(task); - this.completeTask(task, idle); - task.reject(err); - return; - } - - this.dispatch(); - } - - processFile(input: ProcessFileInput) { - // ProcessFile doesn't currently use cancellation, but we could add it later - return this.enqueue("processFile", input); - } - - encodeQuery(text: string, signal?: AbortSignal) { - return this.enqueue("encodeQuery", { text }, signal); - } - - rerank(input: TaskPayloads["rerank"], signal?: AbortSignal) { - return this.enqueue("rerank", input, signal); - } - - async destroy(): Promise { - if (this.destroyPromise) return this.destroyPromise; - if (this.destroyed) return; - - this.destroyed = true; - - for (const task of this.tasks.values()) { - this.clearTaskTimeout(task); - task.reject(new Error("Worker pool destroyed")); - } - this.tasks.clear(); - this.taskQueue = []; - - const killPromises = this.workers.map( - (w) => - new Promise((resolve) => { - w.child.removeAllListeners("message"); - w.child.removeAllListeners("exit"); - w.child.once("exit", () => resolve()); - w.child.kill("SIGTERM"); - const force = setTimeout(() => { - try { - w.child.kill("SIGKILL"); - } catch {} - }, FORCE_KILL_GRACE_MS); - setTimeout(() => { - clearTimeout(force); - resolve(); - }, WORKER_TIMEOUT_MS); - }), - ); - - this.destroyPromise = Promise.allSettled(killPromises).then(() => { - this.workers = []; - this.destroyPromise = null; - }); - - await this.destroyPromise; - } -} - -let singleton: WorkerPool | null = null; - -export function getWorkerPool(): WorkerPool { - if (!singleton) { - singleton = new WorkerPool(); - } - return singleton; -} - -export async function destroyWorkerPool(): Promise { - if (!singleton) return; - const pool = singleton; - singleton = null; - await pool.destroy(); -} - -export function isWorkerPoolInitialized(): boolean { - return singleton !== null; -} diff --git a/src/lib/workers/process-child.ts b/src/lib/workers/process-child.ts deleted file mode 100644 index 25e5ccd1..00000000 --- a/src/lib/workers/process-child.ts +++ /dev/null @@ -1,70 +0,0 @@ -import process from "node:process"; -import processFile, { - encodeQuery, - type ProcessFileInput, - type ProcessFileResult, - type RerankDoc, - rerank, -} from "./worker"; - -type IncomingMessage = - | { id: number; method: "processFile"; payload: ProcessFileInput } - | { id: number; method: "encodeQuery"; payload: { text: string } } - | { - id: number; - method: "rerank"; - payload: { query: number[][]; docs: RerankDoc[]; colbertDim: number }; - }; - -type OutgoingMessage = - | { id: number; result: ProcessFileResult } - | { id: number; result: Awaited> } - | { id: number; result: Awaited> } - | { id: number; error: string } - | { id: number; heartbeat: true }; - -const send = (msg: OutgoingMessage) => { - if (process.send) { - process.send(msg); - } -}; - -process.on("message", async (msg: IncomingMessage) => { - const { id, method, payload } = msg; - try { - if (method === "processFile") { - const onProgress = () => { - send({ id, heartbeat: true }); - }; - const result = await processFile(payload, onProgress); - send({ id, result }); - return; - } - if (method === "encodeQuery") { - const result = await encodeQuery(payload); - send({ id, result }); - return; - } - if (method === "rerank") { - const result = await rerank(payload); - send({ id, result }); - return; - } - send({ id, error: `Unknown method: ${method}` }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - send({ id, error: message }); - } -}); - -process.on("uncaughtException", (err) => { - console.error("[process-worker] uncaughtException", err); - process.exitCode = 1; - process.exit(); -}); - -process.on("unhandledRejection", (reason) => { - console.error("[process-worker] unhandledRejection", reason); - process.exitCode = 1; - process.exit(); -}); diff --git a/src/lib/workers/worker.ts b/src/lib/workers/worker.ts deleted file mode 100644 index 65f284de..00000000 --- a/src/lib/workers/worker.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - type ProcessFileInput, - type ProcessFileResult, - type RerankDoc, - WorkerOrchestrator, -} from "./orchestrator"; - -export type { ProcessFileInput, ProcessFileResult, RerankDoc }; - -const orchestrator = new WorkerOrchestrator(); - -export default async function processFile( - input: ProcessFileInput, - onProgress?: () => void, -): Promise { - return orchestrator.processFile(input, onProgress); -} - -export async function encodeQuery(input: { text: string }) { - return orchestrator.encodeQuery(input.text); -} - -export async function rerank(input: { - query: number[][]; - docs: RerankDoc[]; - colbertDim: number; -}) { - return orchestrator.rerank(input); -} diff --git a/test_skeleton.py b/test_skeleton.py deleted file mode 100644 index 952cc70b..00000000 --- a/test_skeleton.py +++ /dev/null @@ -1,2 +0,0 @@ -def hello(): - print("world") diff --git a/tests/chunking.test.ts b/tests/chunking.test.ts index a08812dc..d9e9ecb7 100644 --- a/tests/chunking.test.ts +++ b/tests/chunking.test.ts @@ -71,8 +71,7 @@ function example() {}`; }, "/repo/path/file.ts", ); - expect(displayText).toContain("// /repo/path/file.ts"); - expect(displayText).toContain("File: /repo/path/file.ts"); + expect(displayText).toContain("// File: /repo/path/file.ts"); expect(displayText).toContain("code"); }); }); diff --git a/tests/expander.test.ts b/tests/expander.test.ts new file mode 100644 index 00000000..ad91f56a --- /dev/null +++ b/tests/expander.test.ts @@ -0,0 +1,524 @@ +import { describe, expect, it, vi, beforeEach } from "vitest"; +import type { ChunkType } from "../src/lib/store/types"; + +// Mock VectorDB +const mockQuery = vi.fn(); +const mockTable = { + query: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: vi.fn(() => ({ + toArray: mockQuery, + })), + })), + })), +}; + +const mockDb = { + ensureTable: vi.fn(async () => mockTable), +}; + +vi.mock("../src/lib/store/vector-db", () => ({ + VectorDB: vi.fn(() => mockDb), +})); + +import { Expander } from "../src/lib/search/expander"; +import type { ExpandOptions } from "../src/lib/search/expansion-types"; + +describe("Expander", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockQuery.mockResolvedValue([]); + }); + + const createMockChunk = (overrides: Partial = {}): ChunkType => ({ + type: "text", + text: "function test() { return 42; }", + score: 0.9, + metadata: { + path: "src/lib/utils.ts", + hash: "abc123", + }, + generated_metadata: { + start_line: 10, + end_line: 15, + num_lines: 6, + }, + defined_symbols: ["test"], + referenced_symbols: ["helper", "config"], + imports: [], + exports: [], + ...overrides, + }); + + describe("symbol resolution", () => { + it("resolves symbols to correct definitions", async () => { + const expander = new Expander(mockDb as any); + + // Mock definition found for "helper" symbol + mockQuery.mockResolvedValueOnce([ + { + id: "def-helper", + path: "src/lib/helper.ts", + content: "export function helper() {}", + display_text: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + is_anchor: false, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + expect(expanded.expanded.length).toBeGreaterThan(0); + expect(expanded.expanded[0].relationship).toBe("symbols"); + expect(expanded.expanded[0].via).toBe("helper"); + expect(expanded.stats.symbolsResolved).toBeGreaterThan(0); + }); + + it("prefers same-directory definitions", async () => { + const expander = new Expander(mockDb as any); + + // Return two definitions - one in same dir, one in different dir + mockQuery.mockResolvedValueOnce([ + { + id: "def-distant", + path: "src/other/helper.ts", + content: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + }, + { + id: "def-nearby", + path: "src/lib/helper.ts", + content: "export function helper() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: [], + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxExpanded: 1, + }); + + // Should prefer the same-directory definition + expect(expanded.expanded.length).toBe(1); + const expandedPath = + (expanded.expanded[0].chunk.metadata as any)?.path || ""; + expect(expandedPath).toContain("src/lib"); + }); + + it("respects maxExpanded limit", async () => { + const expander = new Expander(mockDb as any); + + // Return many definitions for each symbol query + const manyDefs = Array.from({ length: 50 }, (_, i) => ({ + id: `def-${i}`, + path: `src/file${i}.ts`, + content: `export function func${i}() {}`, + start_line: 1, + end_line: 3, + defined_symbols: [`func${i}`], + referenced_symbols: [], + })); + + // Mock returns subset of definitions for each query + mockQuery.mockImplementation(() => Promise.resolve(manyDefs.slice(0, 5))); + + const results = [ + createMockChunk({ + referenced_symbols: manyDefs.map((_, i) => `func${i}`), + }), + ]; + + const maxExpanded = 5; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxExpanded, + }); + + // Should respect the limit + expect(expanded.expanded.length).toBeLessThanOrEqual(maxExpanded); + }); + + it("handles circular references without infinite loop", async () => { + const expander = new Expander(mockDb as any); + + // Chunk A refs B, query returns chunk that refs A + mockQuery.mockResolvedValueOnce([ + { + id: "def-helper", + path: "src/lib/helper.ts", + content: "export function helper() { test(); }", + start_line: 1, + end_line: 3, + defined_symbols: ["helper"], + referenced_symbols: ["test"], // References back! + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 2, + }); + + // Should complete without error and not include duplicates + const ids = new Set( + expanded.expanded.map( + (e) => (e.chunk.metadata as any)?.hash || e.chunk.text, + ), + ); + expect(ids.size).toBe(expanded.expanded.length); + }); + + it("returns empty expansion gracefully when no definitions found", async () => { + const expander = new Expander(mockDb as any); + + // No definitions found + mockQuery.mockResolvedValue([]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + // Should return original results with empty expanded + expect(expanded.original).toEqual(results); + expect(expanded.expanded).toEqual([]); + expect(expanded.stats.symbolsResolved).toBe(0); + }); + }); + + describe("caller expansion", () => { + it("finds callers that reference defined symbols", async () => { + const expander = new Expander(mockDb as any); + + // Mock caller found + mockQuery.mockResolvedValueOnce([ + { + id: "caller-1", + path: "src/routes/handler.ts", + content: "import { test } from '../lib/utils'; test();", + start_line: 5, + end_line: 10, + defined_symbols: ["handleRequest"], + referenced_symbols: ["test"], + is_anchor: false, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["callers"], + }); + + expect(expanded.expanded.length).toBeGreaterThan(0); + expect(expanded.expanded[0].relationship).toBe("callers"); + expect(expanded.expanded[0].via).toContain("uses"); + expect(expanded.stats.callersFound).toBeGreaterThan(0); + }); + }); + + describe("neighbor expansion", () => { + it("includes anchor chunks from same directory", async () => { + const expander = new Expander(mockDb as any); + + // First query is for symbols (returns nothing) + mockQuery.mockResolvedValueOnce([]); + // Second query for callers (returns nothing) + mockQuery.mockResolvedValueOnce([]); + // Third query for neighbors + mockQuery.mockResolvedValueOnce([ + { + id: "neighbor-anchor", + path: "src/lib/other.ts", + content: "// File: src/lib/other.ts\nExports: helper", + start_line: 0, + end_line: 5, + defined_symbols: [], + referenced_symbols: [], + is_anchor: true, + }, + ]); + + const results = [createMockChunk()]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols", "callers", "neighbors"], + }); + + const neighbors = expanded.expanded.filter( + (e) => e.relationship === "neighbors", + ); + expect(neighbors.length).toBeGreaterThanOrEqual(0); + if (neighbors.length > 0) { + expect(neighbors[0].via).toBe("same directory"); + expect(expanded.stats.neighborsAdded).toBeGreaterThan(0); + } + }); + }); + + describe("multi-hop expansion", () => { + it("follows chains of dependencies with depth > 1", async () => { + const expander = new Expander(mockDb as any); + + // Depth 1: A -> B + mockQuery + .mockResolvedValueOnce([ + { + id: "def-B", + path: "src/lib/b.ts", + content: "export function funcB() { funcC(); }", + start_line: 1, + end_line: 3, + defined_symbols: ["funcB"], + referenced_symbols: ["funcC"], + }, + ]) + // Depth 2: B -> C + .mockResolvedValueOnce([ + { + id: "def-C", + path: "src/lib/c.ts", + content: "export function funcC() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["funcC"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["funcB"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 2, + }); + + // Should have found both B and C + expect(expanded.expanded.length).toBeGreaterThanOrEqual(1); + }); + + it("scores decay with depth", async () => { + const expander = new Expander(mockDb as any); + + // Return definitions at different depths + mockQuery.mockResolvedValue([ + { + id: "def-1", + path: "src/lib/dep.ts", + content: "export function dep() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["dep"], + referenced_symbols: [], + }, + ]); + + const results = [createMockChunk({ referenced_symbols: ["dep"] })]; + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxDepth: 1, + }); + + // All expanded chunks should have score < 1 (decayed from parent) + for (const node of expanded.expanded) { + expect(node.score).toBeLessThan(1.0); + } + }); + }); + + describe("token budgeting", () => { + it("stops expanding when token budget exhausted", async () => { + const expander = new Expander(mockDb as any); + + // Return large chunks + const largeDefs = Array.from({ length: 10 }, (_, i) => ({ + id: `def-${i}`, + path: `src/file${i}.ts`, + content: "x".repeat(1000), // ~250 tokens each + start_line: 1, + end_line: 100, + defined_symbols: [`func${i}`], + referenced_symbols: [], + })); + + mockQuery.mockResolvedValue(largeDefs); + + const results = [ + createMockChunk({ + referenced_symbols: largeDefs.map((_, i) => `func${i}`), + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxTokens: 500, // Should only fit 1-2 chunks + }); + + expect(expanded.stats.totalTokens).toBeLessThanOrEqual(500 + 100); // Some buffer + expect(expanded.stats.budgetRemaining).toBeDefined(); + }); + + it("prioritizes high-value chunks within budget", async () => { + const expander = new Expander(mockDb as any); + + mockQuery.mockResolvedValue([ + { + id: "def-nearby", + path: "src/lib/nearby.ts", + content: "export function nearby() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["nearby"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["nearby"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + maxTokens: 5000, // Generous budget + }); + + // Should include the nearby definition with high score + if (expanded.expanded.length > 0) { + expect(expanded.expanded[0].score).toBeGreaterThan(0); + } + }); + }); + + describe("graceful degradation", () => { + it("returns original results when database unavailable", async () => { + const failingDb = { + ensureTable: vi.fn(async () => { + throw new Error("Database unavailable"); + }), + }; + + const expander = new Expander(failingDb as any); + const results = [createMockChunk()]; + + const expanded = await expander.expand(results, "test query"); + + expect(expanded.original).toEqual(results); + expect(expanded.expanded).toEqual([]); + expect(expanded.truncated).toBe(false); + }); + + it("continues expansion when individual queries fail", async () => { + const expander = new Expander(mockDb as any); + + // First query fails, second succeeds + mockQuery + .mockRejectedValueOnce(new Error("Query failed")) + .mockResolvedValueOnce([ + { + id: "def-success", + path: "src/lib/success.ts", + content: "export function success() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["success"], + referenced_symbols: [], + }, + ]); + + const results = [ + createMockChunk({ + referenced_symbols: ["failing", "success"], + }), + ]; + + const expanded = await expander.expand(results, "test query", { + strategies: ["symbols"], + }); + + // Should still have results from successful query + expect(expanded.expanded.length).toBeGreaterThanOrEqual(0); + }); + }); +}); + +describe("ExpandOptions", () => { + it("uses default options when none provided", async () => { + const expander = new Expander(mockDb as any); + mockQuery.mockResolvedValue([]); + + const results = [ + { + type: "text" as const, + text: "test", + score: 0.9, + metadata: { path: "test.ts", hash: "abc" }, + generated_metadata: { start_line: 1, end_line: 5 }, + defined_symbols: [], + referenced_symbols: [], + }, + ]; + + const expanded = await expander.expand(results, "query"); + + // Should complete without error using defaults + expect(expanded.original).toEqual(results); + }); + + it("respects custom strategies", async () => { + const expander = new Expander(mockDb as any); + + mockQuery.mockResolvedValue([ + { + id: "def-1", + path: "src/lib/dep.ts", + content: "export function dep() {}", + start_line: 1, + end_line: 3, + defined_symbols: ["dep"], + referenced_symbols: [], + }, + ]); + + const results = [ + { + type: "text" as const, + text: "function test() { dep(); }", + score: 0.9, + metadata: { path: "test.ts", hash: "abc" }, + generated_metadata: { start_line: 1, end_line: 5 }, + defined_symbols: ["test"], + referenced_symbols: ["dep"], + }, + ]; + + // Only use symbols strategy + const expanded = await expander.expand(results, "query", { + strategies: ["symbols"], + }); + + // Should only have symbol expansions + for (const node of expanded.expanded) { + expect(node.relationship).toBe("symbols"); + } + expect(expanded.stats.callersFound).toBe(0); + expect(expanded.stats.neighborsAdded).toBe(0); + }); +}); diff --git a/tests/git-worktree.test.ts b/tests/git-worktree.test.ts new file mode 100644 index 00000000..3bab62fc --- /dev/null +++ b/tests/git-worktree.test.ts @@ -0,0 +1,86 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { getGitCommonDir, getMainRepoRoot, isWorktree } from "../src/lib/utils/git"; + +function makeTempDir(prefix: string): string { + return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); +} + +function writeFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content, "utf-8"); +} + +function makeWorktreeFixture() { + const root = makeTempDir("osgrep-worktree-fixture-"); + const mainRepoRoot = path.join(root, "Documents", "GitHub", "repo"); + const worktreeRoot = path.join( + root, + "conductor", + "workspaces", + "repo", + "feature-branch", + ); + const mainGitDir = path.join(mainRepoRoot, ".git"); + const worktreeGitDir = path.join(mainGitDir, "worktrees", "feature-branch"); + + fs.mkdirSync(mainGitDir, { recursive: true }); + fs.mkdirSync(worktreeRoot, { recursive: true }); + fs.mkdirSync(worktreeGitDir, { recursive: true }); + writeFile(path.join(worktreeGitDir, "commondir"), "../.."); + + return { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir }; +} + +describe("git worktree detection", () => { + const created: string[] = []; + + afterEach(() => { + for (const dir of created.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it("does not treat a normal repo root as a worktree", () => { + const { root, mainRepoRoot } = makeWorktreeFixture(); + created.push(root); + + expect(isWorktree(mainRepoRoot)).toBe(false); + expect(getGitCommonDir(mainRepoRoot)).toBeNull(); + expect(getMainRepoRoot(mainRepoRoot)).toBeNull(); + }); + + it("detects a worktree via gitfile even when outside the main repo tree", () => { + const { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir } = + makeWorktreeFixture(); + created.push(root); + + writeFile(path.join(worktreeRoot, ".git"), `gitdir: ${worktreeGitDir}\n`); + const expectedMainGitDir = fs.realpathSync.native(mainGitDir); + const expectedMainRepoRoot = fs.realpathSync.native(mainRepoRoot); + + expect(isWorktree(worktreeRoot)).toBe(true); + expect(getGitCommonDir(worktreeRoot)).toBe(expectedMainGitDir); + expect(getMainRepoRoot(worktreeRoot)).toBe(expectedMainRepoRoot); + }); + + if (process.platform === "win32") { + it.skip("detects a worktree when `.git` is a symlinked directory", () => {}); + } else { + it("detects a worktree when `.git` is a symlinked directory", () => { + const { root, mainRepoRoot, mainGitDir, worktreeRoot, worktreeGitDir } = + makeWorktreeFixture(); + created.push(root); + + fs.symlinkSync(worktreeGitDir, path.join(worktreeRoot, ".git")); + const expectedMainGitDir = fs.realpathSync.native(mainGitDir); + const expectedMainRepoRoot = fs.realpathSync.native(mainRepoRoot); + + expect(isWorktree(worktreeRoot)).toBe(true); + expect(getGitCommonDir(worktreeRoot)).toBe(expectedMainGitDir); + expect(getMainRepoRoot(worktreeRoot)).toBe(expectedMainRepoRoot); + }); + } +});