diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..79e416da --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# Weekly dependency refresh — replaces reactive minimum-fix patching. +# Grouped so minor+patch ride one PR; majors stay individual for review. +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 10 + groups: + cargo-minor-and-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/bump-develop.yml b/.github/workflows/bump-develop.yml index e31f704b..d195302c 100644 --- a/.github/workflows/bump-develop.yml +++ b/.github/workflows/bump-develop.yml @@ -37,12 +37,8 @@ on: types: [closed] branches: [develop] -permissions: - contents: write - -# Serialize concurrent merges so each one gets its own bump. cancel-in-progress: -# false → a queued run waits, then re-reads a fresh develop (already carrying the -# previous bump) before bumping again. So N rapid merges → N patch bumps. +# Aikido 30640694: no workflow-level permissions; the single job below gets +# exactly what it needs (contents:write for the bump push). concurrency: group: bump-develop cancel-in-progress: false @@ -52,6 +48,8 @@ jobs: # Only when the PR was actually merged (closed-without-merge is a no-op). if: github.event.pull_request.merged == true runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout develop # pin@v4 @@ -62,6 +60,9 @@ jobs: # the "block develop" ruleset may push to develop. See SETUP note above. # Passing `token:` also persists those creds so `git push` below works. token: ${{ secrets.CI_PAT }} + # Aikido 35039595: explicit intent — this job DOES push the bump commit, + # so credentials must persist (see "How do I fix it?" guidance). + persist-credentials: true - name: Compute next patch version id: ver diff --git a/AGENTS.develop.md b/AGENTS.develop.md index 249349d3..39c30d7b 100644 --- a/AGENTS.develop.md +++ b/AGENTS.develop.md @@ -235,14 +235,14 @@ Never `unwrap_or_default()` a store error on a search path — "no results" and | Branch | Description | |---|---| -| `chore/claude-hook-glob-and-docs` | make Claude Code hook promises match what is enforced | -| `feat/web-guard-topic-scoped` | scope web-guard to the mounted products only | -| `fix/web-guard-ps1-topic-scoping` | topic-scope `web-guard.ps1`, per-mount retry cache | +| *(none)* | | --- ## Changelog highlights (recent) +- **v1.3.37** — per-index embedding models end-to-end: serve queries, `POST /repos` and CLI index/stats/status honour the model each index records in its `metadata.json`; `serve --model` sets the default for newly created indexes; unrecorded indexes are queried with the built-in model plus a caller-facing warning; mid-rebuild indexes no longer report ready (PR #248) +- **v1.3.23–v1.3.36** — dependency + platform wave: rmcp 3.3, fastembed 6.1 + ort rc.13, tantivy 0.26, axum 0.8, ratatui 0.30 + crossterm 0.29, thiserror 2, notify 8, tree-sitter 0.27, dirs/sha2/scip/sysinfo refresh + dependabot (weekly); clears the open Aikido/RUSTSEC advisories - **v1.3.19** — `find_impact` ambiguity envelope + `resolved_symbol`; partial-results `warnings`; C# symbol-key uniqueness (index v2.0) - **v1.3.16** — REST `/find-impact` endpoint - **v1.3.3** — federation hardening release diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fbf67f1..9633ee3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,40 @@ more PRs land; when the release is actually tagged, the same section is finalized in place with a date — no renaming/migration step needed. --> +## [1.4.0] - 2026-09-17 + +### Security + +- **Dependency refresh: h2, rustls, quinn-proto, zerovec-derive, moka.** A semver-safe `cargo update` lifts 126 packages within their existing requirements, clearing five open advisories without code changes: h2 0.4.15→0.4.19 (Aikido 41883526), rustls 0.23.42→0.23.45 (RUSTSEC-2026-0285), quinn-proto 0.11.16→0.11.18 (Aikido 41883527), zerovec-derive 0.11.3→0.11.6 (Aikido 41883531) and moka 0.12.15→0.12.16 (Aikido 41297220). fastembed/ort are deliberately kept at 5.17.3/rc.12 — 5.17.4 hard-requires the unstable ort rc.13 API break, which moves in its own PR. + +- **rmcp 1.8.0 → 3.3.0 — clears the five Aikido advisories on the MCP SDK (34247111).** Behavior-identical upgrade: the legacy `initialize` handshake and session semantics stay the default (`ProtocolVersion::LATEST` remains 2025-11-25); the 2026-07-28 stateless lifecycle is opt-in upstream and deliberately not enabled here. Code impact stayed small because the `#[tool_router]`/`#[tool_handler]` macros absorb the new MRTR response enums: `Content`/`RawContent` are now `ContentBlock`, the constructors-only impl takes `#[tool_router(allow_empty)]`, the stdio proxy's manual `call_tool` returns `CallToolResponse`, and content assertions drop the removed `.raw` projection. + +### Changed + +- **ort rc.13 + fastembed 5.17.4 (ONNX runtime refresh).** fastembed 5.17.4 hard-requires ort 2.0.0-rc.13, so both move together. rc.13 renamed the CPU execution provider (`CPUExecutionProvider` → `ort::ep::CPU`); the embedder's import and construction follow, builder chain unchanged (arena allocator still on). Scope note: ort-sys rc.13 still pins `lzma-rust2 ^0.15`, so the lzma-rust2 0.16.5 advisory (Aikido 37515813) remains open until upstream bumps its build dependency. + +- **Vector-index stack aligned on rand 0.10: arroy 0.5→0.8, heed 0.20→0.22.** The direct `rand` requirement moves to 0.10.2 — the major rmcp already uses — instead of pinning three rand majors side by side in the lock. arroy 0.8 and heed 0.22 follow because arroy 0.8 hard-requires both (`rand ^0.10.2`, `heed ^0.22.1`); no call-site changes were needed beyond silencing heed's `EnvFlags::NO_TLS` deprecation (same LMDB `MDB_NOTLS` flag, same behavior — the type-state `Env` migration is deferred). heed-types 0.21, roaring 0.11 and ordered-float 5 ride along transitively. Note: the rand 0.9.5 Aikido flagged rides the fastembed→hf-hub/tokenizers chain and clears with the fastembed 6 major, not with this change. + +- **Embedding stack majors: fastembed 6.1, hf-hub 1.0, ndarray 0.17.** Zero call-site changes — the codesearch embedder sits on fastembed's `ModelType`/`InitOptions` surface, which 6.1 kept stable. Scope note: fastembed 6.1 still resolves `image 0.25`, so the weezl 0.1.12/moxcms 0.8.1 advisories (Aikido 30640676/37515815) remain open until fastembed adopts image 0.26+; likewise rand 0.9.5 stays via hf-hub 0.5/tokenizers. + +- **tantivy 0.22 → 0.26 with graceful FTS reset.** The collector API changed so `TopDocs::with_limit` needs `.order_by_score()` at the three call sites. Because tantivy cannot open index files written by an older major, an unreadable FTS index is now wiped and recreated as a fresh empty index instead of failing the whole DB open — the FTS index is derived data, BM25 results rebuild on the next (re)index, and a warning is logged pointing at `codesearch index`. Vector search and all non-FTS paths are unaffected; pinned by a regression test that feeds the store a corrupt `meta.json`. + +- **TUI stack majors: ratatui 0.30, crossterm 0.29.** Zero call-site changes — the serve TUI sits on stable surface (`CrosstermBackend`, `Terminal`, `TableState`, `Paragraph`, `Layout`). This removes the last lru 0.12.5 path (ratatui's chain now carries lru 0.18.4; tantivy stays on the 0.16.4 that upstream 0.26 pins). + +- **axum 0.7 → 0.8.** The only breaking surface hit: path parameters changed syntax from `:param` to `{param}` — all route registrations (`/repos/:alias*`, `/chunk/:id`) and the shared `CHUNK_PATH` constant move to brace syntax, including the federation client's URL templating that derives from the same constant. Extractors, middleware and `axum::serve` compile unchanged; the serve + federation test suites exercise the rebuilt router end-to-end. + +- **thiserror 1.0 → 2.0 (direct).** Drop-in for all error enums (`#[error]`, `#[from]` unchanged); tantivy's chain still carries thiserror 1.x transitively until upstream moves. + +- **File-watch stack majors: notify 6.1 → 8.2, notify-debouncer-full 0.3 → 0.7.** The debouncer absorbed `Watcher` into `Debouncer` itself (`debouncer.watch()/unwatch()` replace `.watcher().watch()`), and root cache tracking is now automatic, so the explicit `cache().add_root()` call goes away. The watcher's cache type now follows upstream's per-platform recommendation (`RecommendedCache`: `FileIdMap` on Windows/macOS, `NoCache` on Linux — file-ID tracking is an internal rename-detection optimization; event mapping never reads file IDs directly). Also removes `mio 0.8.11` from the lock entirely (it rode the Linux-only inotify 0.9 path; notify 8 uses inotify 0.11). + +- **tree-sitter 0.26 → 0.27, tree-sitter-proto 0.4 → 0.6.** Zero call-site changes — the chunker sits on the stable `Parser`/`Language` surface and grammars load through the ABI-stable `LANGUAGE.into()` (`tree-sitter-language`) route, so all 17 grammar crates stay pinned while the core moves a major. + +- **Small majors batch: dirs 7, sha2 0.11, scip 0.10, sysinfo 0.39; dead `tower`/`tower-http` direct deps removed.** dirs/scip/sysinfo were drop-in. sha2 0.11's digest arrays no longer implement `LowerHex`, so the two hash-to-hex sites (`file_meta.rs`, `chunker/mod.rs`) hex-encode the digest bytes explicitly — output unchanged. `tower` and `tower-http` were declared as direct dependencies but never imported anywhere (CORS/trace middleware never wired in); removing them shrinks the direct dependency surface (both remain in the lock transitively via axum/reqwest/hf-hub, which is upstream's business). + +### Fixed + +- **Serve auto-recovers LMDB storage-format corruption with a sequential wipe + rebuild.** After the arroy 0.5→0.8 / heed 0.20→0.22 major upgrades, every repo whose on-disk database was written by the previous binary failed its symbol rebuild with `MDB_BAD_VALSIZE: Unsupported size of key/DB name/data, or wrong DUPFIXED size` (observed on all C# repos after deploy). When a symbol rebuild now fails with that error class, serve wipes the repo's DB directory — closing the LMDB envs first via the same eviction sequence `remove_repo` uses, with the same bounded retry for transient Windows lock holders — and force-reindexes it through the existing force-reindex machinery, whose store-open path recreates everything on the new formats. Recoveries are queued and processed strictly **one repo at a time**: each rebuild runs a full CPU-bound embed pass, so parallel recoveries would thrash the machine. Read-only repos are skipped with a pointer to the owning writer. This closes the gap the tantivy FTS graceful reset (above) already covered on the FTS side — the vector/symbol stores now self-heal the same upgrade boundary instead of staying red until an operator force-reindexes by hand. + ## [1.3.19] ### Changed @@ -24,8 +58,22 @@ finalized in place with a date — no renaming/migration step needed. - **`find_impact` surfaces partial results.** Index warnings — non-compiling C# projects, swallowed reference-resolution exceptions, `scip-typescript` non-zero exits with usable output — now persist (C#: alongside the cached refs in the same transaction; TS: in the index meta) and ride the answer as an additive `warnings` array instead of vanishing into the log, so consumers such as the `audit` binary's `impact`/`removals` never read incomplete evidence as zero (todo #139). +- **Claude Code web-guard hook is topic/mount-scoped.** Only queries about the mounted products reach the web guard, and the retry cache is keyed per mount. + +- **`codesearch serve --model X` sets the default model for newly created indexes.** Previously `--model` was inert on `serve` (each repo's query model comes from its own `metadata.json`); now it is the model a repo is indexed with when it is added through serve without an explicit model — `POST /repos`, including the `codesearch index add` path delegated to a running serve — so an operator can make a non-default model the norm without passing `--model` on every add. An index that already records its own model is never overridden (an explicit `model` in the request still wins). Serve reports the default in `GET /status` as `default_model` and at startup. It is deliberately **not** the query fallback for a repo whose `metadata.json` records no model: a legacy index built before the model-recording contract is queried with the built-in 384-dim default, and the search response carries a warning naming the assumed model and the re-index command. Following the serve default there would break a legacy repo the instant an operator set `--model` to a model of another dimension (a 384-dim index queried with a 768-dim model fails), and degrade it silently for a same-dimension model. + ### Fixed +- **`status` no longer reports `ready` for an index that cannot be searched.** Readiness keyed only off `total_chunks`, so a repo mid-rebuild — chunks inserted, `build_index()` not yet run — reported `status: "ready"` / "Index is ready for searching." while every search failed with `Index not built. Call build_index() after inserting chunks.` Both the single-repo and group status paths now require the vector index (`stats.indexed`) to be built before reporting `ready`, otherwise they report `building` with a message that says the vector index is not built. A store that failed to report stats is still surfaced separately (degraded-ready with `warnings`), never misread as "not built". + +- **Adding a repo with `--model` now creates the index at that model's dimension.** `POST /repos` — the path `codesearch index add --model …` delegates to when serve is running — opened the store with the default 384-dim dimension and applied the model override only to `metadata.json` afterwards. The background reindex then embedded 768-dim EmbeddingGemma vectors into a 384-dim store and indexed nothing: the `.codesearch.db` directory existed, `stats` showed the new dimension, and no files were indexed. The store is now opened at the override's dimension. + +- **The CLI no longer downgrades a non-default index to the 384-dim default.** `codesearch index` resolved its embedding model as `--model`-or-`ModelType::default()` and never consulted the model recorded in the index's `metadata.json`, so re-indexing a repo built with EmbeddingGemma embedded 384-dim MiniLM vectors against a 768-dim store (and `FileMetaStore` logged "Model changed, full re-index required", wiping the file metadata). The CLI now resolves the recorded model through the same helper the serve/watcher paths use; an explicit `--model` that disagrees is rejected with a pointer to `--force`. Relatedly, `codesearch stats`, `get_db_stats` and the repo listing opened the vector store with a hardcoded 384, so `Dimensions:` always read 384 for every index — they now read the recorded dimensions. + +- **`status(kind="index")` reports the routed repo's model, not the service default.** The `model` field was the service's own model (the hardcoded default in serve mode) while `dimensions` came from live store stats, so every repo read `minilm-l6-q` regardless of what it was indexed with. It now resolves per repo for `project=`, and reports the common model — or `mixed` — for a group. + +- **Serve mode embeds each query with its target repo's indexed model.** The multi-repo MCP service built its shared embedder from `ModelType::default()` (384-dim MiniLM) and ignored both `--model` and the `model_short_name` each index records, so every semantic query against an index rebuilt with a 768-dim model (EmbeddingGemma) failed with `Query embedding dimension mismatch: expected 768, got 384` — and a same-dimension mismatch would have silently compared incomparable vector spaces. The service now resolves the model per routed repo (the same `metadata.json` contract the indexing path already followed) through a per-model `EmbeddingServicePool`; group fan-out embeds the query once per distinct model and searches each store with its own. (See the `serve --model` default for newly created indexes under **Added**.) + - **Persisted helper-exit warnings are platform-stable — Linux CI green again.** `ExitStatus`'s `Display` renders `exit code: N` on Windows but `exit status: N` on Unix, so the non-zero-exit warning the TypeScript symbol indexer persists into its meta table disagreed with its own regression test on Linux, failing `test-linux`/`csharp-integration-tests` deterministically since #238. A shared `exit_status_text()` renders `exit code: N` on every platform (signal-terminated processes fall back to the platform string), applied to the persisted TS warning and the C#/TS helper log lines, pinned by a cross-platform unit test. - **C# canonical symbol keys no longer collapse distinct declarations.** Generic arity (``M`1``), the full containing-type chain and fully qualified parameter types are part of the key again, so overloads that previously shared one identity keep their own. The index version is bumped to 2.0 with a key-format stamp in the index meta: a stale-format index reports as absent and rebuilds once on upgrade (todo #139). diff --git a/Cargo.lock b/Cargo.lock index 09b2f4ed..f2ee5e04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,9 +24,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -57,9 +57,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -126,6 +126,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -160,22 +169,24 @@ checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "arroy" -version = "0.5.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc5f272f38fa063bbff0a7ab5219404e221493de005e2b4078c62d626ef567e" +checksum = "2f88e18ede2cb2f69e1a68ea804c8fc2f19f748da5f28c795271f279f4068afb" dependencies = [ "bytemuck", "byteorder", + "enum-iterator", "heed", - "log", "memmap2", "nohash", - "ordered-float", - "rand 0.8.7", + "ordered-float 5.5.0", + "page_size", + "rand 0.10.2", "rayon", "roaring", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", + "tracing", ] [[package]] @@ -189,13 +200,22 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", ] [[package]] @@ -225,7 +245,7 @@ dependencies = [ "num-traits", "pastey 0.1.1", "rayon", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "y4m", ] @@ -255,9 +275,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -265,9 +285,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -278,13 +298,13 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.9" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ - "async-trait", "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -297,8 +317,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -312,19 +331,17 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ - "async-trait", "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -343,6 +360,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "base64ct" version = "1.8.3" @@ -358,6 +381,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bit_field" version = "0.10.3" @@ -372,9 +410,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" dependencies = [ "serde_core", ] @@ -397,6 +435,19 @@ dependencies = [ "no_std_io2", ] +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -406,6 +457,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -415,13 +475,37 @@ dependencies = [ "objc2", ] +[[package]] +name = "bon" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60eafe0d77c3a2fc292c1d1346c3041b33c0a108085a2afabf672b70f69dbbc9" +dependencies = [ + "bon-macros", +] + +[[package]] +name = "bon-macros" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0f9631d8aaaee112c41985d675ef269e02acbd4f33122836af4f0c5f699ff6" +dependencies = [ + "darling 0.24.1", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", + "regex-automata", "serde_core", ] @@ -437,6 +521,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + [[package]] name = "bytemuck" version = "1.25.2" @@ -448,13 +538,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -475,12 +565,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - [[package]] name = "cast" version = "0.3.0" @@ -498,9 +582,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.3.0" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", @@ -528,12 +612,12 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -580,9 +664,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -590,9 +674,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -602,21 +686,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cmake" @@ -629,7 +713,7 @@ dependencies = [ [[package]] name = "codesearch" -version = "1.3.21" +version = "1.4.0" dependencies = [ "anyhow", "arroy", @@ -638,47 +722,45 @@ dependencies = [ "bincode", "chrono", "clap", - "colored", + "colored 2.2.0", "criterion", "crossterm", "ctrlc", "dashmap", - "dirs 5.0.1", + "dirs 7.0.0", "fastembed", "fs2", "heed", - "hf-hub 0.3.2", + "hf-hub 1.0.0", "ignore", "indicatif 0.17.11", "libc", "moka", - "ndarray 0.16.1", + "ndarray", "notify", "notify-debouncer-full", "num_cpus", "ort", "pretty_assertions", "protobuf", - "rand 0.8.7", + "rand 0.10.2", "ratatui", "rayon", "regex", - "reqwest 0.13.4", + "reqwest 0.13.5", "rmcp", "schemars", "scip", "serde", "serde_json", "serial_test", - "sha2", - "sysinfo", + "sha2 0.11.0", + "sysinfo 0.39.6", "tantivy", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "tokio", "tokio-util", - "tower", - "tower-http", "tracing", "tracing-appender", "tracing-subscriber", @@ -727,27 +809,22 @@ dependencies = [ ] [[package]] -name = "combine" -version = "4.6.7" +name = "colored" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "bytes", - "memchr", + "windows-sys 0.61.2", ] [[package]] -name = "compact_str" -version = "0.8.2" +name = "combine" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd622ebbb56a5b2ccb651b32b911cdeb2a9b4b11776b2473bf26a26a286244e" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", + "bytes", + "memchr", ] [[package]] @@ -774,27 +851,63 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.0", + "unicode-width", "windows-sys 0.59.0", ] [[package]] name = "console" -version = "0.16.4" +version = "0.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.0", + "unicode-width", "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "const-str" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" + +[[package]] +name = "const_panic" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ "percent-encoding", "time", @@ -845,6 +958,21 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "countio" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9702aee5d1d744c01d82f6915644f950f898e014903385464c773b96fefdecb" +dependencies = [ + "futures-io", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -856,18 +984,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] @@ -908,20 +1036,26 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -929,39 +1063,41 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crossterm_winapi", - "mio 1.2.2", + "derive_more", + "document-features", + "mio", "parking_lot", - "rustix 0.38.44", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -992,6 +1128,35 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" +dependencies = [ + "link-section", + "linktime-proc-macro", +] + [[package]] name = "ctrlc" version = "3.5.2" @@ -999,10 +1164,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", - "nix", + "nix 0.31.3", "windows-sys 0.61.2", ] +[[package]] +name = "daachorse" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5614204febbc33cc07a2806aa6440b904ac012b68eecc37f4493ea4a76455a3d" + [[package]] name = "darling" version = "0.20.11" @@ -1015,12 +1186,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", + "darling_core 0.24.1", + "darling_macro 0.24.1", ] [[package]] @@ -1039,15 +1210,15 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1063,13 +1234,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.23.0", + "darling_core 0.24.1", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1095,11 +1266,23 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "datasketches" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c286de4e81ea2590afc24d754e0f83810c566f50a1388fa75ebd57928c0d9745" + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + [[package]] name = "der" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ "pem-rfc7468", "zeroize", @@ -1145,6 +1328,28 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + [[package]] name = "diff" version = "0.1.13" @@ -1157,17 +1362,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", ] [[package]] -name = "dirs" -version = "5.0.1" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "dirs-sys 0.4.1", + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -1176,19 +1383,16 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys 0.5.0", + "dirs-sys", ] [[package]] -name = "dirs-sys" -version = "0.4.1" +name = "dirs" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" dependencies = [ - "libc", - "option-ext", - "redox_users 0.4.6", - "windows-sys 0.48.0", + "dirs-sys", ] [[package]] @@ -1199,7 +1403,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users", "windows-sys 0.61.2", ] @@ -1209,7 +1413,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "block2", "libc", "objc2", @@ -1217,13 +1421,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1237,9 +1441,9 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "doxygen-rs" @@ -1264,9 +1468,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encode_unicode" @@ -1276,11 +1480,37 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1309,6 +1539,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -1326,15 +1567,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6" [[package]] -name = "exr" -version = "1.74.2" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "exr" +version = "1.74.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.8.9", "num-complex", "pulp", "rayon-core", @@ -1342,6 +1592,16 @@ dependencies = [ "zune-inflate", ] +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastdivide" version = "0.4.2" @@ -1350,18 +1610,18 @@ checksum = "9afc2bd4d5a73106dd53d10d73d3401c2f32730ba2c0b93ddb888a8983680471" [[package]] name = "fastembed" -version = "5.17.3" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c8600c9ec79b51d60c19911fe14eac04fe9c2895e87d2a3e80e2213d645a32" +checksum = "bf2876a78b0fa3a9095baf8bbec83126b3c5b7eab8fc4e1e7ad9e68b14ed5c8a" dependencies = [ - "anyhow", "hf-hub 0.5.0", "image", - "ndarray 0.17.2", + "ndarray", "ort", "safetensors", "serde", "serde_json", + "thiserror 2.0.20", "tokenizers", ] @@ -1396,29 +1656,43 @@ dependencies = [ ] [[package]] -name = "filetime" -version = "0.2.29" +name = "filedescriptor" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" dependencies = [ - "cfg-if", "libc", + "thiserror 1.0.69", + "winapi", ] [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.9.1", + "zlib-rs", ] [[package]] @@ -1427,12 +1701,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1475,12 +1743,12 @@ dependencies = [ [[package]] name = "fs4" -version = "0.8.4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" +checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" dependencies = [ - "rustix 0.38.44", - "windows-sys 0.52.0", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -1500,9 +1768,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1515,9 +1783,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1525,15 +1793,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1542,38 +1810,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1586,6 +1854,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gearhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616e8f476a586f1b078d9eece21f7a071f27997e709e2b471f5697deea53bda9" + [[package]] name = "generic-array" version = "0.14.7" @@ -1605,7 +1879,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1645,11 +1919,31 @@ dependencies = [ "weezl", ] +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "globset" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -1660,9 +1954,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -1696,33 +1990,33 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.1.5", + "foldhash", + "serde", + "serde_core", ] [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", - "serde", - "serde_core", + "foldhash", ] [[package]] -name = "hashbrown" -version = "0.17.1" +name = "heapify" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +checksum = "0049b265b7f201ca9ab25475b22b47fe444060126a51abe00f77d986fc5cc52e" [[package]] name = "heck" @@ -1732,11 +2026,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "heed" -version = "0.20.5" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" +checksum = "ad82d6598ccf1dac15c8b758a1bd282b755b6776be600429176757190a1b0202" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "heed-traits", "heed-types", @@ -1757,9 +2051,9 @@ checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" [[package]] name = "heed-types" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d3f528b053a6d700b2734eabcd0fd49cb8230647aa72958467527b0b7917114" +checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" dependencies = [ "bincode", "byteorder", @@ -1770,26 +2064,15 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] -name = "hf-hub" -version = "0.3.2" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b780635574b3d92f036890d8373433d6f9fc7abb320ee42a5c25897fc8ed732" -dependencies = [ - "dirs 5.0.1", - "indicatif 0.17.11", - "log", - "native-tls", - "rand 0.8.7", - "serde", - "serde_json", - "thiserror 1.0.69", - "ureq 2.12.1", -] +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hf-hub" @@ -1807,11 +2090,65 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.19", - "ureq 3.3.0", + "thiserror 2.0.20", + "ureq", "windows-sys 0.61.2", ] +[[package]] +name = "hf-hub" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ccb6bcc85dec15413ef5949879f9a5497ca4568ed702547eb83fac23376e5c" +dependencies = [ + "base64 0.22.1", + "bon", + "bytes", + "futures", + "getrandom 0.2.17", + "globset", + "hf-xet", + "hyper", + "pathdiff", + "percent-encoding", + "reqwest 0.13.5", + "serde", + "serde_json", + "sha2 0.11.0", + "thiserror 2.0.20", + "tokio", + "tokio-retry", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "hf-xet" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c237ef4fb0ce1962a5117f8bd8c74454b41629826a9df17d14a1840ca18f0754" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "http", + "more-asserts", + "serde", + "serde_json", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "uuid", + "xet-client", + "xet-core-structures", + "xet-data", + "xet-runtime", +] + [[package]] name = "hmac-sha256" version = "1.1.14" @@ -1826,9 +2163,9 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1846,9 +2183,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1869,11 +2206,26 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -1973,9 +2325,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1987,9 +2339,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2000,9 +2352,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2014,16 +2366,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2034,15 +2387,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2082,9 +2435,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f8a7b8211e695a1d0cd91cace480d4d0bd57667ab10277cc412c5f7f4884f83" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -2132,18 +2485,20 @@ dependencies = [ [[package]] name = "imgref" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" +checksum = "6e44b0a4eaa4c82f441d50a963f2d5f05a787240aeee097597033e72accfd22f" [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -2155,7 +2510,7 @@ dependencies = [ "console 0.15.11", "number_prefix", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width", "web-time", ] @@ -2165,9 +2520,9 @@ version = "0.18.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ - "console 0.16.4", + "console 0.16.6", "portable-atomic", - "unicode-width 0.2.0", + "unicode-width", "unit-prefix", "web-time", ] @@ -2183,11 +2538,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.2", "inotify-sys", "libc", ] @@ -2203,27 +2558,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "indoc", "proc-macro2", "quote", - "syn 2.0.119", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", + "syn 3.0.5", ] [[package]] @@ -2237,11 +2580,20 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is-terminal" @@ -2269,24 +2621,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -2314,7 +2648,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link", ] @@ -2363,20 +2697,48 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", "wasm-bindgen", ] +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.20", +] + +[[package]] +name = "konst" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f660d5f887e3562f9ab6f4a14988795b694099d66b4f5dedc02d197ba9becb1d" +dependencies = [ + "const_panic", + "konst_proc_macros", + "typewit", +] + +[[package]] +name = "konst_proc_macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" + [[package]] name = "kqueue" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" dependencies = [ "kqueue-sys", "libc", @@ -2388,10 +2750,16 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", ] +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + [[package]] name = "lazy_static" version = "1.5.0" @@ -2434,18 +2802,33 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "6480ccc157a1389bb2e4891b24751b0f798ba640d22386f23143fbcc89da195a" dependencies = [ "libc", ] [[package]] -name = "linux-raw-sys" -version = "0.4.15" +name = "line-clipping" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "link-section" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" + +[[package]] +name = "linktime-proc-macro" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" [[package]] name = "linux-raw-sys" @@ -2455,9 +2838,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -2487,9 +2870,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "loop9" @@ -2502,24 +2885,36 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.5" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.17.1", ] [[package]] name = "lru-slab" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +checksum = "4050469837a6ff301cd14c1f8f24f88549e6d548f24f64e2148eb0f72cebc51f" [[package]] name = "lz4_flex" -version = "0.11.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373f5eceeeab7925e0c1098212f2fbc4d416adec9d35051a6ab251e824c1854a" +checksum = "7ef0d4ed8669f8f8826eb00dc878084aa8f253506c4fd5e8f58f5bce72ddb97e" +dependencies = [ + "twox-hash", +] [[package]] name = "lzma-rust2" @@ -2527,21 +2922,31 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619" +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + [[package]] name = "macro_rules_attribute" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65049d7923698040cd0b1ddcced9b0eb14dd22c5f86ae59c3740eab64a676520" +checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c" dependencies = [ "macro_rules_attribute-proc_macro", - "paste", + "pastey 0.2.3", ] [[package]] name = "macro_rules_attribute-proc_macro" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670fdfda89751bc4a84ac13eaa63e205cf0fd22b4c9a5fbfa085b63c1f1d3a30" +checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c" [[package]] name = "matchers" @@ -2554,9 +2959,9 @@ dependencies = [ [[package]] name = "matchit" -version = "0.7.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "matrixmultiply" @@ -2580,11 +2985,10 @@ dependencies = [ [[package]] name = "measure_time" -version = "0.8.3" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbefd235b0aadd181626f281e1d684e116972988c14c264e42069d5e8a5775cc" +checksum = "51c55d61e72fc3ab704396c5fa16f4c184db37978ae4e94ca8959693a235fc0e" dependencies = [ - "instant", "log", ] @@ -2604,11 +3008,36 @@ dependencies = [ ] [[package]] -name = "mime" -version = "0.3.17" +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2626,34 +3055,32 @@ dependencies = [ ] [[package]] -name = "mio" -version = "0.8.11" +name = "miniz_oxide" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", + "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2688,6 +3115,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "more-asserts" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fafa6961cabd9c63bcd77a45d7e3b7f3b552b70417831fb0f56db717e72407e" + [[package]] name = "moxcms" version = "0.8.1" @@ -2698,6 +3131,33 @@ dependencies = [ "pxfm", ] +[[package]] +name = "multiversion" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" +dependencies = [ + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "murmurhash32" version = "0.3.1" @@ -2721,21 +3181,6 @@ dependencies = [ "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 = "ndarray" version = "0.17.2" @@ -2757,13 +3202,26 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nix" version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -2811,37 +3269,44 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "notify" -version = "6.1.1" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.13.1", - "crossbeam-channel", - "filetime", + "bitflags 2.13.2", "fsevent-sys", "inotify", "kqueue", "libc", "log", - "mio 0.8.11", + "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] name = "notify-debouncer-full" -version = "0.3.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7fd166739789c9ff169e654dc1501373db9d80a4c3f972817c8a4d7cf8f34e" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" dependencies = [ - "crossbeam-channel", "file-id", "log", "notify", - "parking_lot", + "notify-types", "walkdir", ] +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.13.2", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -2899,9 +3364,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2924,7 +3389,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -2937,6 +3401,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -2958,7 +3431,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", + "dispatch2", + "objc2", ] [[package]] @@ -2967,6 +3442,16 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "objc2", +] + [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -2977,6 +3462,26 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3001,7 +3506,7 @@ version = "6.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "libc", "once_cell", "onig_sys", @@ -3029,7 +3534,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "foreign-types", "libc", @@ -3081,35 +3586,53 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" +dependencies = [ + "num-traits", +] + [[package]] name = "ort" -version = "2.0.0-rc.12" +version = "2.0.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133" +checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b" dependencies = [ - "ndarray 0.17.2", + "ndarray", "ort-sys", "smallvec", "tracing", - "ureq 3.3.0", + "ureq", ] [[package]] name = "ort-sys" -version = "2.0.0-rc.12" +version = "2.0.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90" +checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872" dependencies = [ "hmac-sha256", "lzma-rust2", - "ureq 3.3.0", + "ureq", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +dependencies = [ + "memchr", ] [[package]] name = "ownedbytes" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558" +checksum = "2fbd56f7631767e61784dc43f8580f403f4475bd4aaa4da003e6295e1bab4a7e" dependencies = [ "stable_deref_trait", ] @@ -3124,6 +3647,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "palette" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64" +dependencies = [ + "approx", + "libm", + "palette_derive", + "palette_math", +] + +[[package]] +name = "palette_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "palette_math" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12" +dependencies = [ + "libm", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -3165,6 +3721,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -3180,6 +3742,48 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pest_meta" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e" +dependencies = [ + "pest", +] + [[package]] name = "phf" version = "0.11.3" @@ -3190,6 +3794,16 @@ dependencies = [ "phf_shared", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + [[package]] name = "phf_generator" version = "0.11.3" @@ -3197,7 +3811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.7", + "rand 0.8.8", ] [[package]] @@ -3222,6 +3836,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3230,9 +3864,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plotters" @@ -3268,33 +3902,33 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3324,6 +3958,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.5", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -3418,19 +4062,19 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quinn" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +checksum = "4051e23e9185c255a7e33ef59cdbca87a22d359052eecd22fc6b901fb37d9d11" dependencies = [ "bytes", "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.3", + "rustc-hash", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -3438,9 +4082,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "a9746dbde176634f4f2f1faf2404e30a31b2bc1e9cafb5329c95d8177a18c9fc" dependencies = [ "aws-lc-rs", "bytes", @@ -3449,11 +4093,11 @@ dependencies = [ "rand 0.10.2", "rand_pcg", "ring", - "rustc-hash 2.1.3", + "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -3496,12 +4140,10 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ - "libc", - "rand_chacha 0.3.1", "rand_core 0.6.4", ] @@ -3511,7 +4153,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "rand_chacha 0.9.0", + "rand_chacha", "rand_core 0.9.5", ] @@ -3526,16 +4168,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_chacha" version = "0.9.0" @@ -3551,9 +4183,6 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] [[package]] name = "rand_core" @@ -3570,16 +4199,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "rand_distr" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" -dependencies = [ - "num-traits", - "rand 0.8.7", -] - [[package]] name = "rand_pcg" version = "0.10.2" @@ -3591,35 +4210,115 @@ dependencies = [ [[package]] name = "ratatui" -version = "0.29.0" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d" dependencies = [ - "bitflags 2.13.1", - "cassowary", - "compact_str 0.8.2", - "crossterm", - "indoc", "instability", - "itertools 0.13.0", - "lru", - "paste", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termina", + "ratatui-termwiz", + "ratatui-widgets", + "serde", +] + +[[package]] +name = "ratatui-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c" +dependencies = [ + "bitflags 2.13.2", + "compact_str", + "critical-section", + "hashbrown 0.17.1", + "itertools 0.14.0", + "kasuari", + "lru 0.18.4", + "palette", + "serde", "strum", + "thiserror 2.0.20", "unicode-segmentation", "unicode-truncate", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] -name = "rav1e" -version = "0.8.1" +name = "ratatui-crossterm" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0" dependencies = [ - "aligned-vec", - "arbitrary", - "arg_enum_proc_macro", - "arrayvec", + "cfg-if", + "crossterm", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termina" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2" +dependencies = [ + "instability", + "ratatui-core", + "termina", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1" +dependencies = [ + "bitflags 2.13.2", + "hashbrown 0.17.1", + "indoc", + "instability", + "itertools 0.14.0", + "line-clipping", + "ratatui-core", + "serde", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", "av-scenechange", "av1-grain", "bitstream-io", @@ -3638,9 +4337,9 @@ dependencies = [ "paste", "profiling", "rand 0.9.5", - "rand_chacha 0.9.0", + "rand_chacha", "simd_helpers", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "wasm-bindgen", ] @@ -3666,7 +4365,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -3713,23 +4412,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "redb" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "4ba239c1c1693315d3cc0e601db3b3965543afbf48c41730fdca2f069f510f4a" dependencies = [ - "bitflags 2.13.1", + "libc", ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "getrandom 0.2.17", - "libredox", - "thiserror 1.0.69", + "bitflags 2.13.2", ] [[package]] @@ -3740,27 +4437,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -3777,9 +4474,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3837,14 +4534,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -3853,6 +4552,8 @@ dependencies = [ "hyper-util", "js-sys", "log", + "mime", + "mime_guess", "percent-encoding", "pin-project-lite", "quinn", @@ -3861,6 +4562,7 @@ dependencies = [ "rustls-platform-verifier", "serde", "serde_json", + "serde_urlencoded", "sync_wrapper", "tokio", "tokio-rustls", @@ -3875,6 +4577,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest-middleware" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc3f1384cffa4f274dad2d4ddd73aed32fed8f786d96c6be8aa4e5fd3c3b58" +dependencies = [ + "anyhow", + "async-trait", + "http", + "reqwest 0.13.5", + "thiserror 2.0.20", + "tower-service", +] + [[package]] name = "rgb" version = "0.8.53" @@ -3897,28 +4613,29 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.8.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1f571c72940a19d9532fe52dbea8bc9912bf1d766c2970bb824056b86f3f59" +checksum = "b88db56b8ae316560e9e868b6b978ea940f27cb883323fc90e07435e44158f5c" dependencies = [ "async-trait", - "base64 0.22.1", + "base64 0.23.1", "bytes", "chrono", "futures", "http", "http-body", "http-body-util", + "indexmap", "pastey 0.2.3", "pin-project-lite", "rand 0.10.2", - "reqwest 0.13.4", + "reqwest 0.13.5", "rmcp-macros", "schemars", "serde", "serde_json", "sse-stream", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-stream", "tokio-util", @@ -3929,22 +4646,22 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.8.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aad0035b69380782d78ea95b508327e6deaa2235909053e596eea8f27b5e1d5" +checksum = "873b730df6f0a9b74b13eb514e0dca4c2db0d8b68b74af98a2e9bf3f9d436585" dependencies = [ - "darling 0.23.0", + "darling 0.24.1", "proc-macro2", "quote", "serde_json", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] name = "roaring" -version = "0.10.12" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e8d2cfa184d94d0726d650a9f4a1be7f9b76ac9fdb954219878dc00c1c1e7b" +checksum = "18bd8a37d17a58532776dcdf6041ce64929adca78e8489d5cacbafe99229d3e1" dependencies = [ "bytemuck", "byteorder", @@ -3960,12 +4677,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.3" @@ -3981,37 +4692,24 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", @@ -4037,9 +4735,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -4074,9 +4772,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -4096,6 +4794,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "safe-transmute" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944826ff8fa8093089aba3acb4ef44b9446a99a16f3bf4e74af3f77d340ab7d" + [[package]] name = "safetensors" version = "0.8.0" @@ -4129,9 +4833,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "chrono", "dyn-clone", @@ -4143,21 +4847,21 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f" +checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] name = "scip" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a72133c2d6fd45c9a3a343bcb3db2faa30f68f0919bfa3370ca85add5460c3" +checksum = "d7f651fbd0f98742a47b58623a7bcfd8fad14455912689e4867a1f43ee7e02c6" dependencies = [ "protobuf", ] @@ -4174,7 +4878,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4224,18 +4928,18 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] name = "serde_derive_internals" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -4263,6 +4967,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4308,7 +5023,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -4320,6 +5046,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shellexpand" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" +dependencies = [ + "bstr", + "dirs 6.0.0", + "os_str_bytes", +] + [[package]] name = "shlex" version = "2.0.1" @@ -4343,7 +5080,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 1.2.2", + "mio", "signal-hook", ] @@ -4396,9 +5133,9 @@ checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "sketches-ddsketch" -version = "0.2.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85636c14b73d81f541e525f585c0a2109e6744e1565b5c1668e31c70c10ed65c" +checksum = "513c3f5f732bfd6fbb187619c2dfe9d2f25f1a2976f01d575f0fd329d565df56" dependencies = [ "serde", ] @@ -4411,9 +5148,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "ba467056f1b547ed52077911161fc86985becbc60e8e1857c8a144dab0def891" [[package]] name = "socket2" @@ -4450,9 +5187,9 @@ dependencies = [ [[package]] name = "sse-stream" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c123f296ade4ec4b8b0f6162116e6629f5146922ca5ab40ca9d3c2e73ab4761e" +checksum = "c25ac7aff0abd1dbc474536e40416e1102c7dd9bfba0b9861c6d357f835dcfb4" dependencies = [ "bytes", "futures-util", @@ -4473,6 +5210,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "statrs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3fe7c28c6512e766b0874335db33c94ad7b8f9054228ae1c2abd47ce7d335e" +dependencies = [ + "approx", + "num-traits", +] + [[package]] name = "streaming-iterator" version = "0.1.9" @@ -4487,23 +5234,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn 2.0.119", ] @@ -4519,6 +5265,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.119" @@ -4532,9 +5289,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -4584,13 +5341,28 @@ dependencies = [ "windows", ] +[[package]] +name = "sysinfo" +version = "0.39.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2071df9448915b71c4fe6d25deaf1c22f12bd234f01540b77312bb8e41361e6" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "objc2-open-directory", + "windows", +] + [[package]] name = "system-configuration" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4613,37 +5385,38 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tantivy" -version = "0.22.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96599ea6fccd844fc833fed21d2eecac2e6a7c1afd9e044057391d78b1feb141" +checksum = "861facfabd71044968f364837f9a083b56464ba5a59079f88706ee5c451ca069" dependencies = [ "aho-corasick", "arc-swap", "base64 0.22.1", "bitpacking", + "bon", "byteorder", "census", "crc32fast", "crossbeam-channel", + "datasketches", "downcast-rs", "fastdivide", "fnv", "fs4", "htmlescape", - "itertools 0.12.1", + "itertools 0.14.0", "levenshtein_automata", "log", - "lru", + "lru 0.16.4", "lz4_flex", "measure_time", "memmap2", - "num_cpus", "once_cell", "oneshot", "rayon", "regex", "rust-stemmers", - "rustc-hash 1.1.0", + "rustc-hash", "serde", "serde_json", "sketches-ddsketch", @@ -4656,30 +5429,31 @@ dependencies = [ "tantivy-stacker", "tantivy-tokenizer-api", "tempfile", - "thiserror 1.0.69", + "thiserror 2.0.20", "time", + "typetag", "uuid", "winapi", ] [[package]] name = "tantivy-bitpacker" -version = "0.6.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df" +checksum = "4fed3d674429bcd2de5d0a6d1aa5495fed8afd9c5ecce993019caf7615f53fa4" dependencies = [ "bitpacking", ] [[package]] name = "tantivy-columnar" -version = "0.3.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e" +checksum = "c57166f5bcfd478f370ab8445afb4678dce44801fa5ce5c451aaf8595583c5dc" dependencies = [ "downcast-rs", "fastdivide", - "itertools 0.12.1", + "itertools 0.14.0", "serde", "tantivy-bitpacker", "tantivy-common", @@ -4689,9 +5463,9 @@ dependencies = [ [[package]] name = "tantivy-common" -version = "0.7.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4" +checksum = "bbf10915aa75da3c3b0d58b58853d2e889efbaf32d4982a4c3715dde6bba23e5" dependencies = [ "async-trait", "byteorder", @@ -4713,19 +5487,25 @@ dependencies = [ [[package]] name = "tantivy-query-grammar" -version = "0.22.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82" +checksum = "dfadb8526b6da90704feb293b0701a6aae62ea14983143344be2dc5ce30f1d82" dependencies = [ + "fnv", "nom 7.1.3", + "ordered-float 5.5.0", + "serde", + "serde_json", ] [[package]] name = "tantivy-sstable" -version = "0.3.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e" +checksum = "8a2cfc3ac5164cbadc28965ffb145a8f47582a60ae5897859ad8d4316596c606" dependencies = [ + "futures-util", + "itertools 0.14.0", "tantivy-bitpacker", "tantivy-common", "tantivy-fst", @@ -4734,20 +5514,19 @@ dependencies = [ [[package]] name = "tantivy-stacker" -version = "0.3.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8" +checksum = "6cbb051742da9d53ca9e8fff43a9b10e319338b24e2c0e15d0372df19ffeb951" dependencies = [ "murmurhash32", - "rand_distr", "tantivy-common", ] [[package]] name = "tantivy-tokenizer-api" -version = "0.3.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04" +checksum = "eac258c2c6390673f2685813afeeafcb8c4e0ee7de8dd3fc46838dcc37263f98" dependencies = [ "serde", ] @@ -4761,10 +5540,86 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termina" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e" +dependencies = [ + "bitflags 2.13.2", + "parking_lot", + "rustix", + "signal-hook", "windows-sys 0.61.2", ] +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bitflags 2.13.2", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float 4.6.0", + "pest", + "pest_derive", + "phf", + "sha2 0.10.9", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4776,11 +5631,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -4796,13 +5651,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.5", ] [[package]] @@ -4830,12 +5685,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", @@ -4860,9 +5717,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -4880,28 +5737,19 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" +checksum = "7afbf6e88718afcc138bad01d6ccc3051dbbc3b2ce9793d8b8a3aeb610969cfc" dependencies = [ "ahash", - "aho-corasick", - "compact_str 0.9.1", + "compact_str", + "daachorse", "dary_heap", "derive_builder", "esaxx-rs", @@ -4920,7 +5768,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -4934,7 +5782,7 @@ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", - "mio 1.2.2", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -4945,13 +5793,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -4964,11 +5812,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-retry" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a129d95275ebf4c493ec53bf0f8cd95f5ac161bc4f381700809a54f595d4470" +dependencies = [ + "pin-project-lite", + "rand 0.10.2", + "tokio", +] + [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", "tokio", @@ -5001,15 +5860,39 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.5.3" +name = "tokio_with_wasm" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +checksum = "34e40fbbbd95441133fe9483f522db15dbfd26dc636164ebd8f2dd28759a6aa6" dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", + "js-sys", + "tokio", + "tokio_with_wasm_proc", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "tokio_with_wasm_proc" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d01145a2c788d6aae4cd653afec1e8332534d7d783d01897cefcafe4428de992" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -5022,7 +5905,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "futures-util", "http", @@ -5031,7 +5914,6 @@ dependencies = [ "tower", "tower-layer", "tower-service", - "tracing", "url", ] @@ -5067,7 +5949,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.19", + "thiserror 2.0.20", "time", "tracing-subscriber", ] @@ -5137,13 +6019,12 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.26.11" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1c71c1c4cc0920b20d6b0f6572e7682cd07a6a2faec71067a31fa394c586df" +checksum = "2038684e0058edba0d17302619f62eabce4a8e11c6ac59506996a8d79848851d" dependencies = [ "cc", "regex", - "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -5241,9 +6122,9 @@ dependencies = [ [[package]] name = "tree-sitter-language" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782" +checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081" [[package]] name = "tree-sitter-md" @@ -5267,9 +6148,9 @@ dependencies = [ [[package]] name = "tree-sitter-proto" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e410ccb5fa3cbd6bf7b8e512ecf7ad9d5254395b822bfe9f751b50fa978f31c" +checksum = "9c93b6f1ed20de442e900eb636f2176af6063953dfaa9f76bf168dd3b490a3a1" dependencies = [ "cc", "tree-sitter-language", @@ -5331,12 +6212,66 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "typetag" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90e86058a30d42a1a928dfb4b49bb33c98c3a2b4909492e6b0881cd94798ec2" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f153acc4e99a5f2a5aefa09fb078be54e26271b2813f6041200b224c098d8328" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -5360,21 +6295,15 @@ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" -version = "1.1.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" dependencies = [ - "itertools 0.13.0", + "itertools 0.14.0", "unicode-segmentation", - "unicode-width 0.1.14", + "unicode-width", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" @@ -5401,30 +6330,11 @@ 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", - "url", - "webpki-roots 0.26.11", -] - -[[package]] -name = "ureq" -version = "3.3.0" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "cookie_store", "der", "flate2", @@ -5439,16 +6349,16 @@ dependencies = [ "ureq-proto", "utf8-zero", "webpki-root-certs", - "webpki-roots 1.0.9", + "webpki-roots", ] [[package]] name = "ureq-proto" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +checksum = "5b0809a01d1ca5a51ca70db32bb2a19157582a526505ef3c19e3b343a59aa5ad" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "http", "httparse", "log", @@ -5466,6 +6376,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8-ranges" version = "1.0.5" @@ -5492,10 +6408,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" dependencies = [ + "atomic", "getrandom 0.4.3", "js-sys", "serde_core", @@ -5531,6 +6448,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -5556,6 +6482,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -5565,11 +6500,20 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] + [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -5580,9 +6524,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" dependencies = [ "js-sys", "wasm-bindgen", @@ -5590,9 +6534,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5600,22 +6544,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -5648,9 +6592,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" dependencies = [ "js-sys", "wasm-bindgen", @@ -5675,15 +6619,6 @@ 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.9", -] - [[package]] name = "webpki-roots" version = "1.0.9" @@ -5699,6 +6634,91 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2 0.10.9", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float 4.6.0", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5842,15 +6862,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -5887,21 +6898,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -5944,12 +6940,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5962,12 +6952,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5980,12 +6964,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6010,12 +6988,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6028,12 +7000,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6046,12 +7012,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6064,12 +7024,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6090,9 +7044,150 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "xet-client" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b8da8cc70aa2e3c500c0400e012df82c656ab9fca47f9f939fffc5afd89aca" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.22.1", + "bytes", + "crc32fast", + "futures", + "http", + "hyper", + "more-asserts", + "rand 0.10.2", + "redb", + "reqwest 0.13.5", + "reqwest-middleware", + "serde", + "serde_json", + "serde_repr", + "statrs", + "tempfile", + "thiserror 2.0.20", + "tokio", + "tokio-retry", + "tokio_with_wasm", + "tracing", + "url", + "urlencoding", + "web-time", + "xet-core-structures", + "xet-runtime", +] + +[[package]] +name = "xet-core-structures" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73503c223783dccc864abde22115e09d12f190448a0baf58ab2c54bc709e2f99" +dependencies = [ + "async-trait", + "base64 0.22.1", + "blake3", + "bytemuck", + "bytes", + "countio", + "futures", + "futures-util", + "getrandom 0.4.3", + "heapify", + "itertools 0.14.0", + "lz4_flex", + "more-asserts", + "rand 0.10.2", + "regex", + "safe-transmute", + "serde", + "static_assertions", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tracing", + "uuid", + "web-time", + "xet-runtime", +] + +[[package]] +name = "xet-data" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89052ec5dec2187cad30b86af92cc24fd61c4a57a795f1ff7ff5f38d49184eb" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "gearhash", + "http", + "itertools 0.14.0", + "more-asserts", + "rand 0.10.2", + "serde", + "serde_json", + "sha2 0.11.0", + "tempfile", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "url", + "uuid", + "web-time", + "xet-client", + "xet-core-structures", + "xet-runtime", +] + +[[package]] +name = "xet-runtime" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5c60d5eed38ab4c576f4421bae835e7bd07631fb381705605529d2015c106b" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "colored 3.1.1", + "const-str", + "ctor", + "dirs 6.0.0", + "futures", + "git-version", + "humantime", + "konst", + "libc", + "more-asserts", + "oneshot", + "pin-project", + "rand 0.10.2", + "reqwest 0.13.5", + "serde", + "serde_json", + "shellexpand", + "sysinfo 0.38.4", + "thiserror 2.0.20", + "tokio", + "tokio-util", + "tokio_with_wasm", + "tracing", + "tracing-appender", + "tracing-subscriber", + "web-time", + "whoami", + "winapi", +] [[package]] name = "y4m" @@ -6131,18 +7226,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", @@ -6178,9 +7273,9 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -6189,9 +7284,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -6200,15 +7295,21 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" version = "1.0.23" @@ -6226,18 +7327,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", @@ -6245,9 +7346,9 @@ dependencies = [ [[package]] name = "zune-core" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" [[package]] name = "zune-inflate" diff --git a/Cargo.toml b/Cargo.toml index e94023fd..5b8a818c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codesearch" -version = "1.3.21" +version = "1.4.0" edition = "2021" authors = ["codesearch contributors"] license = "Apache-2.0" @@ -25,19 +25,19 @@ tokio = { version = "1.40", features = ["full"] } tokio-util = { version = "0.7", features = ["rt"] } ctrlc = "3.4" anyhow = "1.0" -thiserror = "1.0" +thiserror = "2.0" # ML & Embeddings -fastembed = "5.0" +fastembed = "6.1" ort = { version = "2.0.0-rc.10", features = ["download-binaries", "copy-dylibs"] } -ndarray = "0.16" -hf-hub = "0.3" +ndarray = "0.17" +hf-hub = "1.0" # Vector DB (temporarily commented due to dep conflicts) # lancedb = "0.5" # Text processing & Parsing -tree-sitter = "0.26.8" +tree-sitter = "0.27" tree-sitter-rust = "0.24.2" tree-sitter-python = "0.25" tree-sitter-javascript = "0.25" @@ -54,12 +54,12 @@ tree-sitter-yaml = "0.7.2" tree-sitter-json = "0.24.8" tree-sitter-md = "0.5.3" tree-sitter-dart = "0.2.0" -tree-sitter-proto = "0.4.0" +tree-sitter-proto = "0.6" # File handling ignore = "0.4" -notify = { version = "6.1", default-features = false, features = ["macos_fsevent"] } -notify-debouncer-full = "0.3" +notify = { version = "8.2", default-features = false, features = ["macos_fsevent"] } +notify-debouncer-full = "0.7" walkdir = "2.5" fs2 = "0.4" # Cross-platform file locking @@ -67,17 +67,15 @@ fs2 = "0.4" # Cross-platform file locking moka = { version = "0.12", features = ["sync"] } # Search & Ranking -tantivy = "0.22" +tantivy = "0.26" regex = "1.12" # Server -axum = "0.7" -tower = "0.5" -tower-http = { version = "0.6", features = ["cors", "trace"] } +axum = "0.8" # TUI -ratatui = "0.29" -crossterm = "0.28" +ratatui = "0.30" +crossterm = "0.29" # Utilities rayon = "1.10" @@ -87,29 +85,30 @@ serde_json = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } tracing-appender = "0.2" -sha2 = "0.10" +sha2 = "0.11" uuid = { version = "1.11", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } colored = "2.1" indicatif = "0.17" -dirs = "5.0" +dirs = "7" num_cpus = "1.16" async-trait = "0.1" # Vector database -arroy = "0.5" -heed = "0.20" +arroy = "0.8" +heed = "0.22" bincode = "1.3" # SCIP symbol indexing — parses standard SCIP protobuf (.scip) emitted by # Sourcegraph indexers (e.g. scip-typescript) for the TypeScript symbol adapter. -scip = "0.9" +scip = "0.10" protobuf = "3.7" -rand = "0.8" -# Bumped floor from 1.5.0 to 1.8.0 for CVE patches (Aikido group: rmcp priority 82, 3 CVEs). -# v2.x is available but is a breaking major bump — deferred. -rmcp = { version = "1.8.0", features = ["server", "client", "transport-io", "transport-streamable-http-server", "transport-streamable-http-client-reqwest", "macros"] } +rand = "0.10" +# Aikido 34247111: 1.8.0 carries 5 advisories (CVE-2026-64684/317735/617985 + 2 more). +# 3.x keeps legacy initialize/session behavior by default (ProtocolVersion::LATEST stays +# V_2025_11_25); the 2026-07-28 stateless lifecycle is opt-in and NOT enabled here. +rmcp = { version = "3.3.0", features = ["server", "client", "transport-io", "transport-streamable-http-server", "transport-streamable-http-client-reqwest", "macros"] } schemars = { version = "1.1.0", features = ["derive"] } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } -sysinfo = { version = "0.38.4", default-features = true } +sysinfo = { version = "0.39", default-features = true } [target.'cfg(unix)'.dependencies] libc = "0.2" diff --git a/README.md b/README.md index f18acdd4..a09b30db 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,21 @@ spaces are model-specific. Keep the same model selected for later indexing runs. Search rejects a `--model` value that differs from the indexed model and points to the required `--force` rebuild instead of mixing incompatible vector spaces. +In **serve** mode the model is resolved **per repository**, from each index's own +metadata, not from a hub-wide setting: a hub may hold indexes built with +different models, and every query is embedded with the model of the repo it +targets (mixed-model groups are fine). `codesearch serve --model ` sets a +**default for newly created indexes**: a repo added without an explicit model +(e.g. `codesearch index add` with no `--model`, delegated to serve) is indexed +with it, and it is reported in `GET /status` as `default_model`. It never +overrides an index that already records its own model — to change an existing +repo's model, re-index that repo +(`codesearch --model index --force`) and restart serve. A repo +whose `metadata.json` records no model (a legacy index built before the +recording contract) is queried with the built-in 384-dim default rather than the +serve default, and the search response carries a warning naming the assumed model +and the re-index command. + ## MCP Configuration codesearch connects to AI agents via MCP. Two modes: diff --git a/integrations/claude-code/hooks/web-guard.ps1 b/integrations/claude-code/hooks/web-guard.ps1 index db7a7e13..6dddaac1 100644 --- a/integrations/claude-code/hooks/web-guard.ps1 +++ b/integrations/claude-code/hooks/web-guard.ps1 @@ -5,13 +5,41 @@ # API / docs questions more precisely — and more currently — than an open web # search. Nothing structurally stops the model from reaching for the always-on # WebSearch/WebFetch tools first, so this hook makes the preference structural: -# the FIRST WebSearch/WebFetch is blocked with actionable guidance; if the same -# query is retried within 5 minutes (i.e. the mounts didn't have the answer), it -# is let through. +# the FIRST WebSearch/WebFetch *about a mounted product* is blocked with +# actionable guidance; a further call about that same mount within 5 minutes +# (i.e. the mounts didn't have the answer) is let through. +# +# TOPIC SCOPING (the important part): a mount only earns the right to intercept +# queries about ITS OWN subject. Mounting cloud/acme says "I have the Acme +# docs indexed" — it says nothing about Rust, Claude Code, or Azure CLI. So the +# hook first decides whether the query is plausibly about a mounted product, and +# stays out of the way entirely when it isn't. A mount's alias is its keyword +# (cloud/acme -> "acme"); anything else it should answer for is declared in +# repos.json under `.remote_mount_topics` — see README. # # Passes through (exit 0, no block) when: # - there are NO remote mounts to steer toward (nothing indexed to prefer) -# - the same query was already blocked in the last 5 minutes +# - the query/URL doesn't mention any mounted product or its declared topics. +# The mounts are a small opt-in allowlist of specific vendor doc sets, so +# they cannot answer a question about an unrelated vendor, a language +# runtime, or a model card. Blocking such a call buys nothing and costs a +# guaranteed wasted round-trip. +# - the same MOUNT was already steered for in the last 5 minutes. Keyed on the +# matched mount, NOT on the exact query string, on purpose: the natural +# follow-up after "the mirror had nothing" is a REFINED web query, and an +# exact-string key treats every refinement as a fresh first attempt and +# blocks it again — punishing precisely the correct behaviour. +# +# TWO SILENT-DEGRADATION TRAPS, both of which turn this hook into a no-op +# without any visible error (an "allow" is silent, so a broken gate looks +# exactly like a working one): +# - NEVER split a mount name with .Split(@('/','\'), [StringSplitOptions]...): +# PowerShell coerces that array into the single-String separator overload and +# looks for the literal "/ \", so nothing ever splits, no alias is ever +# derived, and the gate matches NOTHING. Use `-split` with a regex, or +# .Substring(.LastIndexOf('/') + 1) as below. +# - ALWAYS strip CR from values read out of repos.json. A trailing \r silently +# survives into the alias ("acme`r"), which then never matches anything. # # Windows twin of web-guard.sh. # @@ -33,11 +61,13 @@ $inp = $data.tool_input if ($tool -ne 'WebSearch' -and $tool -ne 'WebFetch') { exit 0 } if ($null -eq $inp) { exit 0 } -# Query (WebSearch) or target URL (WebFetch) — used for the cache key + guidance. +# Query (WebSearch) or target URL (WebFetch) — used for matching, the cache key +# and the guidance text. $names = @($inp.PSObject.Properties.Name) $q = if ($names -contains 'query') { [string]$inp.query } elseif ($names -contains 'url') { [string]$inp.url } else { '' } +if ([string]::IsNullOrWhiteSpace($q)) { exit 0 } # ------------------------------------------------------------------ # 1. Are there any remote doc mounts to steer toward? @@ -50,28 +80,93 @@ $config = if ($env:CODESEARCH_REPOS_CONFIG) { $env:CODESEARCH_REPOS_CONFIG } if (-not (Test-Path $config)) { exit 0 } $mountList = @() +$topicMap = $null try { $cfg = Get-Content $config -Raw | ConvertFrom-Json if ($cfg.PSObject.Properties.Name -contains 'remote_mounts' -and $cfg.remote_mounts) { - $mountList = @($cfg.remote_mounts) + # Strip CR — see the trap note in the header. + $mountList = @($cfg.remote_mounts | ForEach-Object { ([string]$_).Trim("`r", "`n", ' ') } | + Where-Object { $_ }) + } + if ($cfg.PSObject.Properties.Name -contains 'remote_mount_topics') { + $topicMap = $cfg.remote_mount_topics } } catch { exit 0 # unreadable/invalid config -> don't get in the way } if ($mountList.Count -eq 0) { exit 0 } -$mounts = $mountList -join ', ' # ------------------------------------------------------------------ -# 2. Retry cache: same query blocked recently -> let it through. +# 2. Is this query actually ABOUT one of the mounted products? +# +# Keywords per mount: the alias itself, plus any extra terms declared in +# `.remote_mount_topics` (keyed by the full "/" or the bare alias). +# Matching is case-insensitive on a word-ish boundary, so "acme" hits +# "Acme DAM API" and "help.acme.com" but not "acmexyz". +# +# No match -> this simply isn't the mirror's subject -> allow, silently. +# ------------------------------------------------------------------ +$haystack = $q.ToLowerInvariant() + +function Test-KeywordMatch { + param([string]$Haystack, [string]$Keyword) + if ([string]::IsNullOrWhiteSpace($Keyword)) { return $false } + $pattern = '(^|[^a-z0-9])' + [regex]::Escape($Keyword) + '([^a-z0-9]|$)' + return [regex]::IsMatch($Haystack, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) +} + +function Get-MountTopics { + param($TopicMap, [string]$Mount, [string]$Alias) + if ($null -eq $TopicMap) { return @() } + foreach ($key in @($Mount, $Alias)) { + $prop = $TopicMap.PSObject.Properties[$key] + if ($prop -and $prop.Value) { + return @($prop.Value | ForEach-Object { ([string]$_).Trim("`r", "`n", ' ') } | + Where-Object { $_ }) + } + } + return @() +} + +$matched = @() +foreach ($mount in $mountList) { + # Alias = the part after the last '/' — the PowerShell equivalent of the + # shell twin's ${mount##*/}. See the header for why .Split(@('/','\'), ...) + # must NOT be used here. + $aliasName = ([string]$mount).Substring(([string]$mount).LastIndexOf('/') + 1) + + $hit = $false + if (Test-KeywordMatch -Haystack $haystack -Keyword $aliasName) { + $hit = $true + } else { + foreach ($topic in (Get-MountTopics -TopicMap $topicMap -Mount $mount -Alias $aliasName)) { + if (Test-KeywordMatch -Haystack $haystack -Keyword $topic) { + $hit = $true + break + } + } + } + + if ($hit) { $matched += $mount } +} + +if ($matched.Count -eq 0) { exit 0 } + +$relevant = $matched -join ', ' +$primary = $matched[0] + +# ------------------------------------------------------------------ +# 3. Retry cache: same mount steered for recently -> let it through. +# Covers "tried the mounts, they had nothing, now use the web". # ------------------------------------------------------------------ $cacheFile = Join-Path $env:TEMP '.codesearch-web-guard.json' $cacheTTL = 300 # seconds +$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() $cache = @{} if (Test-Path $cacheFile) { try { $stored = Get-Content $cacheFile -Raw | ConvertFrom-Json - $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() foreach ($prop in $stored.PSObject.Properties) { if (($now - [long]$prop.Value) -lt $cacheTTL) { $cache[$prop.Name] = [long]$prop.Value @@ -80,37 +175,45 @@ if (Test-Path $cacheFile) { } catch {} } -$cacheKey = "$q" +# Keyed on tool + primary matched mount, NOT the raw query: one steer per vendor +# per window. An exact-query key made every refinement of the search terms look +# like a first attempt and blocked it again, which is the opposite of the intent. +$cacheKey = "$tool|$primary" if ($cache.ContainsKey($cacheKey)) { - exit 0 # already blocked once this window -> allow the retry + exit 0 # already steered once this window -> allow the follow-up } -$cache[$cacheKey] = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +$cache[$cacheKey] = $now try { $cache | ConvertTo-Json -Compress | Set-Content $cacheFile -NoNewline } catch {} # ------------------------------------------------------------------ -# 3. Block with actionable guidance. +# 4. Block with actionable guidance — naming ONLY the mounts that matched. # ------------------------------------------------------------------ $msg = @" -codesearch has remote documentation mounts — search those before the web. -Mounted remotes: $mounts +This looks like a question about a product whose documentation codesearch has +indexed — search that mirror before the web. +Relevant mount(s): $relevant These indexed mounts often answer product/API/docs questions more precisely -(and more currently) than a web search. Try codesearch first. +(and more currently) than a web search, and they cover vendor sites that need a +login and would fail an anonymous fetch anyway. Step 1 — load the deferred MCP tool schemas (one-time per conversation): ToolSearch("select:mcp__codesearch__search,mcp__codesearch__get_chunk") Step 2 — search the relevant mount (compact=false reads matching content inline): - mcp__codesearch__search(query="$q", project="", compact=false) - mcp__codesearch__get_chunk(chunk_ref="") # full context + mcp__codesearch__search(query="$q", project="$primary", compact=false) + mcp__codesearch__get_chunk(chunk_ref="${primary}:") # full context -Pick the relevant project from the mounted remotes above. +For a canonical source link, read the doc's front-matter chunk (start_line 0) +and cite its ``url:`` field verbatim rather than reconstructing a URL. -This exact $tool call is auto-unblocked if you retry it within 5 minutes -(i.e. the mounts didn't have the answer — go ahead and use the web). +If the mount does NOT have the answer, go straight to the web: any further +$tool call about '$primary' is allowed for the next 5 minutes. You do NOT need +to repeat this call verbatim — refining your search terms is fine and will not +be blocked again. "@ $out = @{ diff --git a/integrations/claude-code/hooks/web-guard.sh b/integrations/claude-code/hooks/web-guard.sh index 3db6663f..6c72a633 100644 --- a/integrations/claude-code/hooks/web-guard.sh +++ b/integrations/claude-code/hooks/web-guard.sh @@ -6,13 +6,26 @@ # API / docs questions more precisely — and more currently — than an open web # search. Nothing structurally stops the model from reaching for the always-on # WebSearch/WebFetch tools first, so this hook makes the preference structural: -# the FIRST WebSearch/WebFetch is blocked with actionable guidance; if the same -# query is retried within 5 minutes (i.e. the mounts didn't have the answer), it -# is let through. +# the FIRST WebSearch/WebFetch *about a mounted product* is blocked with +# actionable guidance; a further call about that same mount within 5 minutes +# (i.e. the mounts didn't have the answer) is let through. +# +# TOPIC SCOPING (the important part): a mount only earns the right to intercept +# queries about ITS OWN subject. Mounting cloud/acme says "I have the Acme +# docs indexed" — it says nothing about Rust, Claude Code, or Azure CLI. So the +# hook first decides whether the query is plausibly about a mounted product, and +# stays out of the way entirely when it isn't. A mount's alias is its keyword +# (cloud/acme -> "acme"); anything else it should answer for is declared in +# repos.json under `.remote_mount_topics` — see README. # # Passes through (exit 0, no block) when: # - there are NO remote mounts to steer toward (nothing indexed to prefer) -# - the same query was already blocked in the last 5 minutes +# - the query/URL doesn't mention any mounted product or its declared topics +# - the same MOUNT was already steered for in the last 5 minutes. Keyed on the +# matched mount, NOT on the exact query string, on purpose: the natural +# follow-up after "the mirror had nothing" is a REFINED web query, and an +# exact-string key treats every refinement as a fresh first attempt and +# blocks it again — punishing precisely the correct behaviour. # # Bash/macOS/Linux twin of web-guard.ps1. Requires: jq # @@ -29,8 +42,10 @@ case "$tool" in *) exit 0 ;; esac -# Query (WebSearch) or target URL (WebFetch) — used for the cache key + guidance. +# Query (WebSearch) or target URL (WebFetch) — used for matching, the cache key +# and the guidance text. q=$(echo "$raw" | jq -r '.tool_input.query // .tool_input.url // empty') +[ -z "$q" ] && exit 0 # ------------------------------------------------------------------ # 1. Are there any remote doc mounts to steer toward? @@ -42,11 +57,61 @@ q=$(echo "$raw" | jq -r '.tool_input.query // .tool_input.url // empty') config="${CODESEARCH_REPOS_CONFIG:-$HOME/.codesearch/repos.json}" [ -f "$config" ] || exit 0 -mounts=$(jq -r '(.remote_mounts // []) | join(", ")' < "$config" 2>/dev/null || true) -[ -z "$mounts" ] && exit 0 +# NOTE: strip CR. On Windows/Git Bash jq emits CRLF, and a trailing \r silently +# survives into the alias ("acme\r"), which then never matches anything — +# the hook degrades to "never block" without any visible error. +mapfile -t mount_list < <(jq -r '(.remote_mounts // [])[]' < "$config" 2>/dev/null | tr -d '\r' || true) +[ "${#mount_list[@]}" -eq 0 ] && exit 0 + +# ------------------------------------------------------------------ +# 2. Is this query actually ABOUT one of the mounted products? +# +# Keywords per mount: the alias itself, plus any extra terms declared in +# `.remote_mount_topics` (keyed by the full "/" or the bare alias). +# Matching is case-insensitive on a word-ish boundary, so "acme" hits +# "Acme DAM API" and "help.acme.com" but not "acmexyz". +# +# No match -> this simply isn't the mirror's subject -> allow, silently. +# ------------------------------------------------------------------ +hay=$(printf '%s' "$q" | tr '[:upper:]' '[:lower:]') + +matches_kw() { + local kw="$1" + [ -z "$kw" ] && return 1 + [[ "$hay" =~ (^|[^a-z0-9])"$kw"([^a-z0-9]|$) ]] +} + +matched=() +for mount in "${mount_list[@]}"; do + [ -z "$mount" ] && continue + alias_name="${mount##*/}" + + hit=0 + if matches_kw "$(printf '%s' "$alias_name" | tr '[:upper:]' '[:lower:]')"; then + hit=1 + else + while IFS= read -r topic; do + [ -z "$topic" ] && continue + if matches_kw "$(printf '%s' "$topic" | tr '[:upper:]' '[:lower:]')"; then + hit=1 + break + fi + done < <(jq -r --arg m "$mount" --arg a "$alias_name" \ + '((.remote_mount_topics // {}) | (.[$m] // .[$a] // []))[]' \ + < "$config" 2>/dev/null | tr -d '\r' || true) + fi + + [ "$hit" -eq 1 ] && matched+=("$mount") +done + +[ "${#matched[@]}" -eq 0 ] && exit 0 + +relevant=$(printf '%s, ' "${matched[@]}") +relevant="${relevant%, }" +primary="${matched[0]}" # ------------------------------------------------------------------ -# 2. Retry cache: same query blocked recently -> let it through. +# 3. Retry cache: same mount steered for recently -> let it through. # Covers "tried the mounts, they had nothing, now use the web". # # NOTE: feed the cache file to jq via stdin redirection (`< file`), never as a @@ -55,7 +120,10 @@ mounts=$(jq -r '(.remote_mounts // []) | join(", ")' < "$config" 2>/dev/null || cache_file="${TMPDIR:-/tmp}/.codesearch-web-guard.json" cache_ttl=300 now=$(date +%s) -cache_key="$q" +# Keyed on tool + primary matched mount, NOT the raw query: one steer per vendor +# per window. An exact-query key made every refinement of the search terms look +# like a first attempt and blocked it again, which is the opposite of the intent. +cache_key="$tool|$primary" if [ -f "$cache_file" ]; then blocked_at=$(jq -r --arg k "$cache_key" '.[$k] // empty' < "$cache_file" 2>/dev/null || true) @@ -75,26 +143,31 @@ else fi # ------------------------------------------------------------------ -# 3. Block with actionable guidance. +# 4. Block with actionable guidance — naming ONLY the mounts that matched. # ------------------------------------------------------------------ msg=$(cat < String { let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); - format!("{:x}", hasher.finalize()) + // sha2 0.11: the digest array no longer impls LowerHex — hex-encode manually + hasher + .finalize() + .iter() + .map(|b| format!("{b:02x}")) + .collect::() } /// TEST METHOD: Estimate memory usage of this chunk in bytes diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 3a42da1f..0b64ced8 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -261,7 +261,9 @@ pub struct Cli { #[arg(long, global = true)] pub store: Option, - /// Embedding model to use (e.g., bge-small, jina-code, embeddinggemma-q4) + /// Embedding model to use (e.g., bge-small, jina-code, embeddinggemma-q4). + /// On `serve`, this is the default for newly created indexes; each repo's + /// existing index keeps the model recorded in its own metadata. #[arg(long, global = true)] pub model: Option, } @@ -1222,10 +1224,19 @@ pub async fn run(cancel_token: CancellationToken) -> Result<()> { if let Err(e) = crate::logger::init_serve_logger(log_level, effective_quiet) { eprintln!("Warning: failed to initialize serve logger: {}", e); } + // `--model` is a global flag inherited by every subcommand. On + // `serve` it sets the serve-wide default for newly created + // indexes — each repo's queries still use the model recorded + // in its own index metadata (a hub may mix models), so this + // never overrides an existing index. + if let Some(mt) = model_type { + warn_if_heavier_model(mt); + } crate::serve::run_serve( host, port, register, + model_type, no_tui, keep_warm_url, idle_suspend_secs, diff --git a/src/constants.rs b/src/constants.rs index 73f27b8f..f0bd457e 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -265,8 +265,8 @@ pub fn resolve_serve_host() -> String { } /// Environment variable to set the admin API key for management endpoints. -/// When set, all management routes (`POST /repos`, `DELETE /repos/:alias`, -/// `POST /repos/:alias/reindex`, `POST /reload`) require this key. +/// When set, all management routes (`POST /repos`, `DELETE /repos/{alias}`, +/// `POST /repos/{alias}/reindex`, `POST /reload`) require this key. /// When unset or empty, management routes are unauthenticated (backward compatible). /// The key is validated against `Authorization: Bearer ` or `X-API-Key: ` headers. pub const SERVE_API_KEY_ENV: &str = "CODESEARCH_SERVE_API_KEY"; @@ -387,8 +387,8 @@ pub const FIND_PATH: &str = "/find"; pub const EXPLORE_PATH: &str = "/explore"; /// REST get-chunk endpoint (HTTP mirror of the `get_chunk` MCP tool). -/// GET `/chunk/:id?context_lines=&project=&group=`. -pub const CHUNK_PATH: &str = "/chunk/:id"; +/// GET `/chunk/{id}?context_lines=&project=&group=`. +pub const CHUNK_PATH: &str = "/chunk/{id}"; /// REST find-impact endpoint (HTTP mirror of the `find_impact` MCP tool). /// POST a `FindImpactRequest` body; returns the tool's JSON payload @@ -570,7 +570,7 @@ pub const DB_DELETE_RETRY_BACKOFF_CAP_MS: u64 = 2000; /// an actually-unlocked directory instead of burning attempts blind. pub const DB_DELETE_ENV_RELEASE_POLL_MS: u64 = 100; -/// Unallocated margin (seconds) the CLI's delegated `DELETE /repos/:alias` +/// Unallocated margin (seconds) the CLI's delegated `DELETE /repos/{alias}` /// request adds on top of serve's legitimate worst-case removal time — /// `DB_DELETE_RETRY_BUDGET_SECS` plus one `BG_TASK_COOPERATIVE_TIMEOUT_SECS` /// per cooperative join (FSW task + index task) — so the CLI receives diff --git a/src/embed/embedder.rs b/src/embed/embedder.rs index f13883a0..d0c2f606 100644 --- a/src/embed/embedder.rs +++ b/src/embed/embedder.rs @@ -1,11 +1,11 @@ use anyhow::{anyhow, Result}; use fastembed::{EmbeddingModel as FastEmbedModel, InitOptions, TextEmbedding}; -use ort::execution_providers::CPUExecutionProvider; +use ort::ep::CPU; use crate::file::Language; /// Available embedding models -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum ModelType { // === MiniLM Family === /// All-MiniLM-L6-v2 - 384 dimensions, fast and efficient @@ -242,6 +242,24 @@ impl ModelType { } } + /// Resolve the embedding model recorded in an index's `metadata.json`. + /// + /// The reader counterpart to [`Self::write_metadata_fields`]. Returns `None` + /// when the file is missing/unreadable, carries no `model_short_name`, or + /// names a model this build does not know — callers fall back to + /// [`ModelType::default`]. Every query path MUST resolve the model per + /// target index through here (instead of assuming the default): embedding a + /// query with any model other than the one the index was built with either + /// fails with a dimension mismatch or silently compares incomparable vector + /// spaces. Multi-repo serve mode is where this matters most, because one + /// hub can hold indexes built with different models. + pub fn from_index_metadata(db_path: &std::path::Path) -> Option { + let content = std::fs::read_to_string(db_path.join("metadata.json")).ok()?; + let json: serde_json::Value = serde_json::from_str(&content).ok()?; + let name = json.get("model_short_name").and_then(|v| v.as_str())?; + Self::parse(name) + } + pub fn prepare_query(&self, text: &str) -> String { match self { Self::EmbeddingGemma300MQ4 => format!("task: search result | query: {text}"), @@ -312,9 +330,7 @@ impl FastEmbedder { // Use CPU execution provider WITH arena allocator for speed. // Arena allocator provides fast memory reuse during inference. - let cpu_ep = CPUExecutionProvider::default() - .with_arena_allocator(true) - .build(); + let cpu_ep = CPU::default().with_arena_allocator(true).build(); let model = TextEmbedding::try_new( InitOptions::new(model_type.to_fastembed_model()) diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 717ed077..bc0a497f 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -10,6 +10,7 @@ pub use cache::{ pub use embedder::{FastEmbedder, ModelType}; use anyhow::Result; +use std::collections::HashMap; use std::env; use std::sync::{Arc, Mutex}; @@ -298,6 +299,50 @@ impl Default for EmbeddingService { } } +/// Lazily-created, per-model cache of [`EmbeddingService`]s. +/// +/// Serve mode is multi-repo and different repos may be indexed with different +/// embedding models (an older MiniLM index alongside a rebuilt EmbeddingGemma +/// one), so a single shared service is wrong: the query must be embedded with +/// the same model the target index was built with. The pool loads each model at +/// most once per serve instance and reuses it across MCP sessions and REST +/// handlers. Each model gets its own mutex, so queries against different models +/// do not serialise on one global lock. +#[derive(Default)] +pub struct EmbeddingServicePool { + services: Mutex>>>, + cache_dir: Option, +} + +impl EmbeddingServicePool { + /// Create a pool. `cache_dir` overrides the ONNX model cache directory + /// (`None` = fastembed's configured cache, i.e. the global models dir). + pub fn new(cache_dir: Option) -> Self { + Self { + services: Mutex::new(HashMap::new()), + cache_dir, + } + } + + /// Return the service for `model`, loading its ONNX model on first use. + /// + /// The returned `Arc` is locked independently per model, so a caller can + /// hold it across an `embed_query` without blocking other models. + pub fn get(&self, model: ModelType) -> Result>> { + let mut guard = self + .services + .lock() + .map_err(|e| anyhow::anyhow!("Embedding service pool mutex poisoned: {e}"))?; + if let Some(existing) = guard.get(&model) { + return Ok(existing.clone()); + } + let service = EmbeddingService::with_cache_dir(model, self.cache_dir.as_deref())?; + let arc = Arc::new(Mutex::new(service)); + guard.insert(model, arc.clone()); + Ok(arc) + } +} + #[cfg(test)] mod tests { use super::*; @@ -308,6 +353,50 @@ mod tests { assert_eq!(model.dimensions(), 384); } + /// The index-metadata reader must invert `write_metadata_fields`, and must + /// report "no answer" (None) for unknown/missing names rather than silently + /// claiming the default — callers decide the fallback. + #[test] + fn test_model_type_round_trips_through_index_metadata() { + for model in [ + ModelType::AllMiniLML6V2Q, + ModelType::EmbeddingGemma300MQ4, + ModelType::BGEBaseENV15, + ] { + let dir = tempfile::tempdir().unwrap(); + let mut obj = serde_json::Map::new(); + model.write_metadata_fields(&mut obj); + std::fs::write( + dir.path().join("metadata.json"), + serde_json::to_string(&obj).unwrap(), + ) + .unwrap(); + assert_eq!( + ModelType::from_index_metadata(dir.path()), + Some(model), + "reader must invert the writer for '{:?}'", + model + ); + } + + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("metadata.json"), + r#"{"model_short_name":"not-a-real-model"}"#, + ) + .unwrap(); + assert_eq!(ModelType::from_index_metadata(dir.path()), None); + + let dir = tempfile::tempdir().unwrap(); + std::fs::write(dir.path().join("metadata.json"), "{}").unwrap(); + assert_eq!(ModelType::from_index_metadata(dir.path()), None); + + assert_eq!( + ModelType::from_index_metadata(std::path::Path::new("/nonexistent-db-dir")), + None + ); + } + #[test] #[ignore] // Requires model download fn test_embedding_service_creation() { diff --git a/src/federation/mod.rs b/src/federation/mod.rs index 897b4152..394cdcbb 100644 --- a/src/federation/mod.rs +++ b/src/federation/mod.rs @@ -184,7 +184,7 @@ pub struct RemoteRepoStatus { pub tool_call_count: Option, } -/// `GET /repos/:alias/info` payload — on-disk index stats for one repo on the +/// `GET /repos/{alias}/info` payload — on-disk index stats for one repo on the /// peer. Only the fields the TUI mount-info overlay renders are typed; every /// field is optional/defaulted so an older/newer remote still parses. #[derive(Debug, Clone, Default, Deserialize, Serialize)] @@ -216,7 +216,7 @@ pub struct RemoteRepoAdded { pub message: Option, } -/// `DELETE /repos/:alias` success payload (HTTP 200). +/// `DELETE /repos/{alias}` success payload (HTTP 200). #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct RemoteRepoRemoved { #[serde(default)] @@ -229,7 +229,7 @@ pub struct RemoteRepoRemoved { pub message: Option, } -/// `POST /repos/:alias/reindex` success payload (HTTP 202). +/// `POST /repos/{alias}/reindex` success payload (HTTP 202). #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct RemoteReindexResult { #[serde(default)] @@ -382,7 +382,7 @@ impl FederationClient { unreachable!("retry loop always returns on its final iteration") } - /// Fetch a single chunk from a remote peer's `/chunk/:id` endpoint. + /// Fetch a single chunk from a remote peer's `/chunk/{id}` endpoint. /// /// Scoping mirrors [`Self::search_project`]: /// - When `remote_alias` is `Some`, the lookup is scoped to that single @@ -402,7 +402,7 @@ impl FederationClient { ) -> Outcome { let mut url = Self::peer_url( peer, - &crate::constants::CHUNK_PATH.replace(":id", &chunk_id.to_string()), + &crate::constants::CHUNK_PATH.replace("{id", &chunk_id.to_string()), ); // Scope the lookup: prefer a single-project scope (`project=`) // so the multi-repo peer can disambiguate the chunk_id; fall back to @@ -476,7 +476,7 @@ impl FederationClient { } /// Shared request/response handling for the management endpoints - /// (`/status`, `/repos`, `/repos/:alias`, `/repos/:alias/reindex`). + /// (`/status`, `/repos`, `/repos/{alias}`, `/repos/{alias}/reindex`). /// /// Distinguishes three failure modes (see [`ManagementOutcome`]): /// transport failure → `Unreachable`; non-2xx → `HttpError` with the peer's @@ -577,7 +577,7 @@ impl FederationClient { .await } - /// `DELETE /repos/:alias` — unregister a repo on the peer and delete its DB. + /// `DELETE /repos/{alias}` — unregister a repo on the peer and delete its DB. /// `alias` is the peer's repo alias (NOT a local path). pub async fn remove_repo( &self, @@ -591,7 +591,7 @@ impl FederationClient { .await } - /// `GET /repos/:alias/info` — fetch on-disk index stats (chunks/files/db + /// `GET /repos/{alias}/info` — fetch on-disk index stats (chunks/files/db /// size/model) for one repo on the peer. `alias` is the peer's repo alias. pub async fn repo_info( &self, @@ -608,7 +608,7 @@ impl FederationClient { .await } - /// `POST /repos/:alias/reindex[?force=true]` — trigger a background + /// `POST /repos/{alias}/reindex[?force=true]` — trigger a background /// incremental (or forced full) reindex of a repo on the peer. pub async fn reindex( &self, @@ -866,7 +866,7 @@ mod tests { // `group`) for a namespaced lookup — the fix for `ambiguous_chunk_id` // on a multi-repo peer. let app = axum::Router::new().route( - "/chunk/:id", + "/chunk/{id}", axum::routing::get( |axum::extract::Query(params): axum::extract::Query< std::collections::HashMap, @@ -918,7 +918,7 @@ mod tests { // lookup must then fall back to the peer's group scope and NOT send a // `project` param — preserving pre-fix behaviour for old refs. let app = axum::Router::new().route( - "/chunk/:id", + "/chunk/{id}", axum::routing::get( |axum::extract::Query(params): axum::extract::Query< std::collections::HashMap, @@ -1041,7 +1041,7 @@ mod tests { async fn remove_repo_targets_alias_in_url() { // Echo the captured alias back to prove it landed in the DELETE path. let app = axum::Router::new().route( - "/repos/:alias", + "/repos/{alias}", axum::routing::delete( |axum::extract::Path(alias): axum::extract::Path| async move { axum::Json(serde_json::json!({ @@ -1072,7 +1072,7 @@ mod tests { async fn reindex_posts_to_alias_reindex_path() { // Capture the alias from the path to prove the reindex URL was built. let app = axum::Router::new().route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post( |axum::extract::Path(alias): axum::extract::Path| async move { axum::Json(serde_json::json!({ @@ -1106,7 +1106,7 @@ mod tests { async fn reindex_with_force_appends_force_query() { // Capture the query string to prove ?force=true was forwarded. let app = axum::Router::new().route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post( |axum::extract::Query(params): axum::extract::Query< std::collections::HashMap, @@ -1140,7 +1140,7 @@ mod tests { // An alias with a space must be percent-encoded on the wire and decoded // back by axum — proves the encoding round-trips through the HTTP layer. let app = axum::Router::new().route( - "/repos/:alias", + "/repos/{alias}", axum::routing::delete( |axum::extract::Path(alias): axum::extract::Path| async move { axum::Json(serde_json::json!({ @@ -1219,7 +1219,7 @@ mod tests { // Transient-status retry (502/503/504 — scale-to-zero cold starts, todo #58) // ========================================================================= - /// Helper: an axum route answering `/chunk/:id` that returns 503 (with a + /// Helper: an axum route answering `/chunk/{id}` that returns 503 (with a /// non-JSON body, like a real cold-starting gateway) for the first /// `fail_first` calls, then a valid 200 JSON chunk payload. Counts every /// hit so tests can assert exactly how many attempts were made. @@ -1230,7 +1230,7 @@ mod tests { let hits = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); let hits_clone = hits.clone(); let router = axum::Router::new().route( - "/chunk/:id", + "/chunk/{id}", axum::routing::get(move || { let hits = hits_clone.clone(); async move { @@ -1340,7 +1340,7 @@ mod tests { let hits = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0)); let hits_clone = hits.clone(); let router = axum::Router::new().route( - "/chunk/:id", + "/chunk/{id}", axum::routing::get(move || { let hits = hits_clone.clone(); async move { diff --git a/src/fts/tantivy_store.rs b/src/fts/tantivy_store.rs index 459980f5..45e7eba4 100644 --- a/src/fts/tantivy_store.rs +++ b/src/fts/tantivy_store.rs @@ -177,16 +177,17 @@ impl FtsStore { /// Open or create index with retry logic for Windows file locking issues fn open_or_create_index_with_retry(fts_path: &Path, schema: &Schema) -> Result { - let max_retries = 3; + const MAX_RETRIES: usize = 3; let mut last_error: Option = None; - for attempt in 0..max_retries { + for attempt in 0..MAX_RETRIES { if attempt > 0 { // Wait before retry (exponential backoff) std::thread::sleep(std::time::Duration::from_millis(100 * (1 << attempt))); } - let result: Result = if fts_path.join("meta.json").exists() { + let opened_existing = fts_path.join("meta.json").exists(); + let result: Result = if opened_existing { Index::open_in_dir(fts_path).map_err(|e| e.to_string()) } else { MmapDirectory::open(fts_path) @@ -202,18 +203,53 @@ impl FtsStore { Err(e) => { last_error = Some(e); // On Windows, try to clear lock files if permission denied - if attempt < max_retries - 1 { + if attempt < MAX_RETRIES - 1 { Self::try_clear_lock_files(fts_path); } } } } - Err(anyhow!( - "Failed to open FTS index after {} retries: {}", - max_retries, - last_error.unwrap_or_default() - )) + // Last resort: a pre-upgrade FTS index written by an older tantivy + // major cannot be opened by this one (format break), which would + // brick the whole DB for existing users. The FTS index is derived + // data — fully rebuildable via reindex — so wipe it and create a + // fresh empty index instead of failing. BM25 results stay empty + // until the next (re)index; vector search is unaffected. + if let Err(wipe_err) = Self::wipe_fts_dir(fts_path) { + return Err(anyhow!( + "Failed to open FTS index after {MAX_RETRIES} retries: {} (wipe also failed: {wipe_err})", + last_error.unwrap_or_default() + )); + } + tracing::warn!( + "FTS index at {} was unreadable by this codesearch version and has been reset; \ + run 'codesearch index' to rebuild it", + fts_path.display() + ); + MmapDirectory::open(fts_path) + .map_err(|e| e.to_string()) + .and_then(|dir| { + Index::create(dir, schema.clone(), IndexSettings::default()) + .map_err(|e| e.to_string()) + }) + .map_err(|e| { + anyhow!( + "Failed to open FTS index after {} retries: {} (fresh create also failed: {})", + MAX_RETRIES, + last_error.unwrap_or_default(), + e + ) + }) + } + + /// Remove the FTS index directory contents so a fresh index can be created. + fn wipe_fts_dir(fts_path: &Path) -> std::io::Result<()> { + if !fts_path.exists() { + return Ok(()); + } + std::fs::remove_dir_all(fts_path)?; + std::fs::create_dir_all(fts_path) } /// Create writer with retry logic for Windows file locking issues @@ -502,7 +538,8 @@ impl FtsStore { }; // Execute search - let top_docs = searcher.search(&parsed_query, &TopDocs::with_limit(limit))?; + let top_docs = + searcher.search(&parsed_query, &TopDocs::with_limit(limit).order_by_score())?; self.collect_fts_results(top_docs) } @@ -560,7 +597,7 @@ impl FtsStore { BooleanQuery::union(vec![Box::new(boosted_sig), Box::new(content_query)]) }; - let top_docs = searcher.search(&combined, &TopDocs::with_limit(limit))?; + let top_docs = searcher.search(&combined, &TopDocs::with_limit(limit).order_by_score())?; self.collect_fts_results(top_docs) } @@ -578,7 +615,7 @@ impl FtsStore { let query = RegexQuery::from_pattern(pattern, self.content_field) .map_err(|e| anyhow!("Invalid regex pattern '{}': {}", pattern, e))?; - let top_docs = searcher.search(&query, &TopDocs::with_limit(limit))?; + let top_docs = searcher.search(&query, &TopDocs::with_limit(limit).order_by_score())?; self.collect_fts_results(top_docs) } @@ -628,7 +665,7 @@ impl FtsStore { Box::new(PhraseQuery::new(terms)) }; - let top_docs = searcher.search(&query, &TopDocs::with_limit(limit))?; + let top_docs = searcher.search(&query, &TopDocs::with_limit(limit).order_by_score())?; self.collect_fts_results(top_docs) } @@ -845,4 +882,32 @@ mod tests { Ok(()) } + + /// An index written by an incompatible tantivy major must not brick the + /// store: the unreadable index is wiped and a fresh empty one created + /// (BM25 rebuilds on the next index run). + #[test] + fn unreadable_index_is_wiped_and_store_recovers() -> Result<()> { + let tmp = tempfile::tempdir()?; + let fts_dir = tmp.path().join("fts"); + std::fs::create_dir_all(&fts_dir)?; + // A meta.json no tantivy version can parse simulates an index from + // an incompatible tantivy major. + std::fs::write(fts_dir.join("meta.json"), "{ not valid tantivy metadata")?; + + let mut store = FtsStore::new(tmp.path())?; + + // Fresh store: opens and searches empty. + let results = store.search("anything", 10, None)?; + assert!(results.is_empty()); + + // And accepts writes after the wipe. + store.add_chunk(1, "recovery probe content", "probe.rs", None, "block")?; + store.commit()?; + let results = store.search("recovery probe", 10, None)?; + assert_eq!(results.len(), 1); + assert_eq!(results[0].chunk_id, 1); + + Ok(()) + } } diff --git a/src/index/manager.rs b/src/index/manager.rs index 23412cb1..c0d54bc4 100644 --- a/src/index/manager.rs +++ b/src/index/manager.rs @@ -504,7 +504,7 @@ impl IndexManager { /// /// Fails fast if metadata is missing, names an unknown model, or records a /// dimension count that disagrees with the resolved model. - fn resolve_embed_model(db_path: &Path) -> Result<(ModelType, usize)> { + pub(crate) fn resolve_embed_model(db_path: &Path) -> Result<(ModelType, usize)> { let metadata_path = db_path.join("metadata.json"); if !metadata_path.exists() { return Err(anyhow::anyhow!( diff --git a/src/index/mod.rs b/src/index/mod.rs index 1855f832..273c460a 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -47,6 +47,58 @@ pub(crate) fn ensure_hnsw_index_if_needed( } } +/// Dimensions recorded in an index's `metadata.json` (fallback: the default). +/// +/// The vector store MUST be opened with the dimensions the index was built +/// with. `codesearch stats` / `get_db_stats` used to pass a hardcoded 384, so +/// they reported `Dimensions: 384` for every index — including 768-dim +/// EmbeddingGemma ones — and opened those stores with the wrong dimension. +fn recorded_dimensions(db_path: &Path) -> usize { + let from_metadata = std::fs::read_to_string(db_path.join("metadata.json")) + .ok() + .and_then(|c| serde_json::from_str::(&c).ok()) + .and_then(|j| j.get("dimensions").and_then(|v| v.as_u64())) + .map(|d| d as usize); + from_metadata + .or_else(|| ModelType::from_index_metadata(db_path).map(|m| m.dimensions())) + .unwrap_or(crate::constants::DEFAULT_EMBEDDING_DIMENSIONS) +} + +/// Resolve the embedding model for an indexing run. +/// +/// On an existing index the model recorded in `metadata.json` wins: embedding +/// with any other model (including the hardcoded default) either fails with a +/// dimension mismatch or silently mixes vector spaces. An explicit `--model` +/// that disagrees with the recorded model is rejected — the index must be +/// rebuilt with `--force` to change models. +/// +/// This mirrors `IndexManager::resolve_embed_model`, which the serve and +/// watcher paths already use; the CLI `index` path used `model.unwrap_or_default()` +/// and therefore downgraded any non-default index to 384-dim MiniLM. +fn resolve_index_model( + db_path: &Path, + force: bool, + requested: Option, +) -> Result { + // `--force` deletes the database (see `get_db_path_smart`), so there is no + // recorded model to honour; a non-existent/legacy index has none either. + if force || !db_path.join("metadata.json").exists() { + return Ok(requested.unwrap_or_default()); + } + let (recorded, _dims) = IndexManager::resolve_embed_model(db_path)?; + match requested { + Some(req) if req != recorded => Err(anyhow::anyhow!( + "Model mismatch: {} was indexed with '{}', but --model '{}' was requested.\n\ + To change models, rebuild the index: codesearch --model {} index --force", + db_path.display(), + recorded.short_name(), + req.short_name(), + req.short_name() + )), + _ => Ok(recorded), + } +} + /// Update metadata.json with current chunk/file counts so that `status(projects)` /// can report accurate numbers without opening LMDB. /// Uses atomic read-modify-write (temp+rename) so a crash never leaves an empty file. @@ -549,7 +601,11 @@ async fn index_with_options( cancel_token: CancellationToken, ) -> Result<()> { let (db_path, project_path) = get_db_path_smart(path, global, force)?; - let model_type = model.unwrap_or_default(); + // Resolve the embedding model BEFORE touching the index: on an existing + // index the model recorded in metadata.json wins, and an explicit `--model` + // that disagrees is rejected (rebuild with --force). Defaulting here would + // embed 384-dim MiniLM vectors into a 768-dim index. See `resolve_index_model`. + let model_type = resolve_index_model(&db_path, force, model)?; // Macro to conditionally print macro_rules! log_print { @@ -714,7 +770,7 @@ async fn index_with_options( if total_chunks_to_delete > 0 { log_print!("\n🔄 Deleting {} old chunks...", total_chunks_to_delete); - let mut store = VectorStore::new(&db_path, 384)?; // Will load dimensions from DB + let mut store = VectorStore::new(&db_path, model_type.dimensions())?; let mut fts_store = FtsStore::new_with_writer(&db_path)?; // Delete deleted files' metadata and chunks @@ -1292,7 +1348,7 @@ pub async fn stats(path: Option) -> Result<()> { println!("💾 Database: {}", db_path.display()); println!("📂 Project: {}", project_path.display()); - let store = VectorStore::new(&db_path, 384)?; // We'll need to store dimensions in metadata + let store = VectorStore::new(&db_path, recorded_dimensions(&db_path))?; let stats = store.stats()?; println!("\n{}", "Vector Store:".bright_green()); @@ -1367,7 +1423,7 @@ fn print_repo_stats(repo_path: &Path, db_path: &Path) -> Result<()> { println!(" 📂 {}", repo_path.display()); // Try to load stats - match VectorStore::new(db_path, 384) { + match VectorStore::new(db_path, recorded_dimensions(db_path)) { Ok(store) => match store.stats() { Ok(stats) => { println!( @@ -1813,7 +1869,7 @@ async fn get_db_stats(db_path: &Path) -> Result { } // Try to get stats from vector store - let store = VectorStore::new(db_path, 384)?; + let store = VectorStore::new(db_path, recorded_dimensions(db_path))?; let stats = store.stats()?; // Calculate database size @@ -1962,8 +2018,8 @@ const SERVE_HEALTH_RETRY_SLEEP: std::time::Duration = std::time::Duration::from_ /// makes. This is required: /// - when serve is bound to a non-localhost address (the `require_auth_for_network` /// middleware guards ALL endpoints, including `/health`), and -/// - for management endpoints (`POST /repos`, `DELETE /repos/:alias`, -/// `POST /repos/:alias/reindex`, `POST /reload`) when serve is bound to +/// - for management endpoints (`POST /repos`, `DELETE /repos/{alias}`, +/// `POST /repos/{alias}/reindex`, `POST /reload`) when serve is bound to /// localhost with the key set. /// /// Without this, delegation to a network-bound serve returns 401 and falls back @@ -2386,7 +2442,7 @@ pub(crate) async fn try_delegate_add_to_serve( } /// The outcome a running serve instance reported for a delegated `index rm` -/// — the parsed `DELETE /repos/:alias` success payload. +/// — the parsed `DELETE /repos/{alias}` success payload. /// /// `db_deleted == false` means the repo is functionally removed (FSW stopped, /// evicted from memory, unregistered from repos.json) but the database @@ -2472,7 +2528,7 @@ pub(crate) async fn try_delegate_rm_to_serve( .map(|(a, _)| a.clone()) .ok_or_else(|| format!("path '{}' not found in repos.json", project_path.display()))?; - // 3. DELETE /repos/:alias + // 3. DELETE /repos/{alias} // // The DELETE needs its OWN client with a timeout that covers serve's // legitimate worst-case removal time: `remove_repo` can spend up to @@ -3154,7 +3210,7 @@ mod remove_order_tests { }), ) .route( - "/repos/:alias", + "/repos/{alias}", axum::routing::delete( |axum::extract::Path(alias): axum::extract::Path| async move { axum::Json(serde_json::json!({ @@ -3232,7 +3288,7 @@ mod remove_order_tests { }), ) .route( - "/repos/:alias", + "/repos/{alias}", axum::routing::delete( |axum::extract::Path(alias): axum::extract::Path| async move { tokio::time::sleep(std::time::Duration::from_secs(4)).await; @@ -3275,3 +3331,112 @@ mod remove_order_tests { assert_global_config_unchanged(_canary); } } + +/// Model resolution for the CLI `index` path: the model recorded in the index's +/// `metadata.json` must win, so re-indexing a non-default index (e.g. rebuilt +/// with EmbeddingGemma) does not silently downgrade it to 384-dim MiniLM. +/// +/// Regression guard for `index_with_options` using `model.unwrap_or_default()`: +/// with the defect reintroduced, `resolve_index_model` returns the default for +/// the gemma case below and the test fails. +#[cfg(test)] +mod index_model_resolution_tests { + use super::*; + + /// Write an index metadata.json recording `model` (as the indexer does). + fn write_metadata(db_path: &Path, model: ModelType) { + std::fs::create_dir_all(db_path).unwrap(); + let mut obj = serde_json::Map::new(); + model.write_metadata_fields(&mut obj); + std::fs::write( + db_path.join("metadata.json"), + serde_json::to_string(&obj).unwrap(), + ) + .unwrap(); + } + + #[test] + fn recorded_dimensions_reads_metadata_dimensions() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(".codesearch.db"); + write_metadata(&db, ModelType::EmbeddingGemma300MQ4); + assert_eq!(recorded_dimensions(&db), 768); + } + + #[test] + fn recorded_dimensions_falls_back_to_model_then_default() { + // Dimensions key absent, model known -> model's dimensions. + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(".codesearch.db"); + std::fs::create_dir_all(&db).unwrap(); + std::fs::write( + db.join("metadata.json"), + r#"{"model_short_name":"embeddinggemma-q4"}"#, + ) + .unwrap(); + assert_eq!(recorded_dimensions(&db), 768); + + // No metadata at all -> default. + let dir = tempfile::tempdir().unwrap(); + assert_eq!( + recorded_dimensions(&dir.path().join(".codesearch.db")), + crate::constants::DEFAULT_EMBEDDING_DIMENSIONS + ); + } + + #[test] + fn resolve_index_model_prefers_recorded_model_on_existing_index() { + // The defect: a bare `codesearch index` on a gemma index picked the + // default (384-dim MiniLM). The recorded model must win. + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(".codesearch.db"); + write_metadata(&db, ModelType::EmbeddingGemma300MQ4); + + assert_eq!( + resolve_index_model(&db, false, None).unwrap(), + ModelType::EmbeddingGemma300MQ4, + "an existing index's recorded model must be used, not the default" + ); + // An explicit --model that agrees is accepted. + assert_eq!( + resolve_index_model(&db, false, Some(ModelType::EmbeddingGemma300MQ4)).unwrap(), + ModelType::EmbeddingGemma300MQ4 + ); + } + + #[test] + fn resolve_index_model_rejects_disagreeing_override_without_force() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(".codesearch.db"); + write_metadata(&db, ModelType::EmbeddingGemma300MQ4); + + let err = resolve_index_model(&db, false, Some(ModelType::AllMiniLML6V2Q)) + .expect_err("a --model that disagrees with the index must be rejected"); + let msg = format!("{err:#}"); + assert!( + msg.contains("Model mismatch") && msg.contains("embeddinggemma-q4"), + "error must name the recorded model and the mismatch, got: {msg}" + ); + + // --force deletes the DB, so the requested model is honoured. + assert_eq!( + resolve_index_model(&db, true, Some(ModelType::AllMiniLML6V2Q)).unwrap(), + ModelType::AllMiniLML6V2Q + ); + } + + #[test] + fn resolve_index_model_uses_requested_model_for_a_fresh_index() { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(".codesearch.db"); // does not exist + + assert_eq!( + resolve_index_model(&db, false, Some(ModelType::EmbeddingGemma300MQ4)).unwrap(), + ModelType::EmbeddingGemma300MQ4 + ); + assert_eq!( + resolve_index_model(&db, false, None).unwrap(), + ModelType::default() + ); + } +} diff --git a/src/lmdb_registry.rs b/src/lmdb_registry.rs index 543a9ccd..1f4cd65a 100644 --- a/src/lmdb_registry.rs +++ b/src/lmdb_registry.rs @@ -42,6 +42,11 @@ use crate::cache::safe_canonicalize; /// Because heed refuses to reopen the same path with different options, this /// must be applied at EVERY env-open site, not only the read-only one — a /// partial rollout would turn a working reopen into an intermittent failure. +// heed 0.22 deprecates the NO_TLS flag in favour of the type-state +// `EnvOpenOptions::read_txn_without_tls()`, but that changes `Env`'s generic +// parameter across the whole registry and the txn Send semantics with it. +// MDB_NOTLS itself is unchanged in LMDB — same flag, same behavior. +#[allow(deprecated)] pub const BASE_ENV_FLAGS: heed::EnvFlags = heed::EnvFlags::NO_TLS; // ── Global registry ───────────────────────────────────────────── diff --git a/src/mcp/explore.rs b/src/mcp/explore.rs index ccc845da..88149237 100644 --- a/src/mcp/explore.rs +++ b/src/mcp/explore.rs @@ -6,7 +6,7 @@ use super::*; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; @@ -40,7 +40,7 @@ impl CodesearchService { let chunk_id = match request.target.parse::() { Ok(id) => id, Err(_) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "For similar mode, `target` must be a numeric chunk_id, got: '{}'", request.target ))])); @@ -54,7 +54,7 @@ impl CodesearchService { }; self.similar_chunks(Parameters(similar_req)).await } - _ => Ok(CallToolResult::success(vec![Content::text(format!( + _ => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown explore kind '{}'. Use `outline` or `similar`.", kind ))])), @@ -138,12 +138,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; // Outline operates on a single repo — reject group fan-out if ctx.is_multi { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( "Tool 'explore' operates on a single repo. Use 'project' instead of 'group'." .to_string(), )])); @@ -151,7 +151,7 @@ impl CodesearchService { if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -177,7 +177,7 @@ impl CodesearchService { { Ok(v) => v, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error reading outline: {e:#}" ))])); } diff --git a/src/mcp/federation_helpers_tests.rs b/src/mcp/federation_helpers_tests.rs index c48d1ff8..01491d7d 100644 --- a/src/mcp/federation_helpers_tests.rs +++ b/src/mcp/federation_helpers_tests.rs @@ -4,7 +4,7 @@ use super::{convert_remote_item, merge_ranked_lists, parse_search_items_from_call_result}; use crate::federation::RemoteSearchItem; use crate::mcp::types::SearchResultItem; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; fn local_item(chunk_id: u32, score: f32) -> SearchResultItem { SearchResultItem { @@ -146,7 +146,7 @@ fn parse_federated_chunk_ref_rejects_garbage() { // === parse_search_items_from_call_result (serve-delegation re-parse) === fn call_result_with_json(json: &str) -> CallToolResult { - CallToolResult::success(vec![Content::text(json.to_string())]) + CallToolResult::success(vec![ContentBlock::text(json.to_string())]) } #[test] diff --git a/src/mcp/find.rs b/src/mcp/find.rs index bbd0497b..673f656c 100644 --- a/src/mcp/find.rs +++ b/src/mcp/find.rs @@ -6,7 +6,7 @@ use super::*; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; @@ -69,7 +69,7 @@ impl CodesearchService { }; self.find_dependents(Parameters(dep_req)).await } - _ => Ok(CallToolResult::success(vec![Content::text(format!( + _ => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown find kind '{}'. Use `definition`, `usages`, `imports`, or `dependents`.", kind ))])), @@ -98,12 +98,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -133,7 +133,7 @@ impl CodesearchService { { Ok(r) => r, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching: {e:#}" ))])); } @@ -141,7 +141,7 @@ impl CodesearchService { }; if fts_results.is_empty() { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result( format!( "No definition found for '{}'. The symbol may not be indexed.", @@ -249,7 +249,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error opening database: {e:#}" ))])); } @@ -298,12 +298,12 @@ impl CodesearchService { // Resolve project/group routing let ctx = match self.resolve_routing(&project, &group, false, "find").await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -332,7 +332,7 @@ impl CodesearchService { { Ok(r) => r, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching: {e:#}" ))])); } @@ -340,7 +340,7 @@ impl CodesearchService { }; if fts_results.is_empty() { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result( format!("No usages found for '{symbol}'. The symbol may not be indexed."), &find_warnings, @@ -422,7 +422,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error opening database: {e:#}" ))])); } diff --git a/src/mcp/find_impact.rs b/src/mcp/find_impact.rs index c4a1c180..035a1957 100644 --- a/src/mcp/find_impact.rs +++ b/src/mcp/find_impact.rs @@ -3,7 +3,7 @@ use super::CodesearchService; use crate::symbols::SymbolReference; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; use std::path::{Path, PathBuf}; @@ -138,7 +138,7 @@ impl CodesearchService { .as_ref() .is_some_and(|s| !s.trim().is_empty()); if !has_name && !has_position && !has_key { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( "Must provide `symbol_name`, both `file` and `line`, or an exact `symbol_key`." .to_string(), )])); @@ -147,7 +147,7 @@ impl CodesearchService { // would let silent precedence decide the answer — the thing this // contract exists to remove. Reject instead. if has_key && (has_name || has_position) { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( "`symbol_key` is mutually exclusive with `symbol_name` and `file`+`line`: pass only the explicit selection.".to_string(), )])); } @@ -158,7 +158,7 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; // Determine project root and db_path for the symbol index @@ -199,7 +199,7 @@ impl CodesearchService { Some(i) => i, None => { let available = registry.available_languages(); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "No symbol indexer for language '{}'. Available languages: {:?}", lang, available ))])); @@ -209,14 +209,14 @@ impl CodesearchService { // No language given and none detectable from a file path. let installed = registry.installed_languages(); if installed.is_empty() { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( "No symbol indexers installed. Install the `scip-csharp` helper for C# support, or `scip-typescript` (via npx) for TypeScript support.".to_string(), )])); } if installed.len() > 1 { // Several helpers installed: answering from one silently is // the same silent pick the ambiguity contract removes. Ask. - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Several symbol indexes are installed ({}). Pass `language` (e.g. \"csharp\") so the lookup cannot silently answer from the wrong one.", installed.join(", ") ))])); @@ -244,7 +244,7 @@ impl CodesearchService { crate::constants::SCIP_CSHARP_HELPER_ENV ), }; - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( serde_json::to_string(&error).unwrap_or_else(|_| error.error.clone()), )])); } @@ -373,7 +373,7 @@ impl CodesearchService { ); let json = serde_json::to_string(&failure).unwrap_or_else(|_| failure.error.clone()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Ok(crate::symbols::KeyMatch::Ambiguous(candidates)) => { let ambiguity = crate::symbols::SymbolAmbiguity { @@ -384,7 +384,7 @@ impl CodesearchService { }; let json = serde_json::to_string(&ambiguity) .unwrap_or_else(|_| "{\"ambiguous\":true}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Ok(crate::symbols::KeyMatch::NotFound) if has_key => { // An explicit key that misses is a loud failure, not an @@ -402,14 +402,14 @@ impl CodesearchService { }; let json = serde_json::to_string(&failure).unwrap_or_else(|_| failure.error.clone()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Ok(crate::symbols::KeyMatch::NotFound) => { // Preserve the historical contract for fuzzy queries: an // unresolvable name/position answers empty references. let impact = build_impact(Vec::new(), None, Vec::new()); let json = serde_json::to_string(&impact).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Ok(crate::symbols::KeyMatch::Resolved(canonical)) => canonical, }; @@ -439,7 +439,7 @@ impl CodesearchService { }; let json = serde_json::to_string(&busy).unwrap_or_else(|_| "{\"busy\":true}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Some(find_impact_tracker::TrackedStatus::Done(Ok(references))) => { tracing::info!( @@ -452,7 +452,7 @@ impl CodesearchService { let warnings = indexer.lookup_warnings(&db_path, &canonical); let impact = build_impact(references, Some(canonical.clone()), warnings); let json = serde_json::to_string(&impact).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } Some(find_impact_tracker::TrackedStatus::Done(Err(chain))) => { // Same classification as a fresh failure: the tracked chain @@ -463,7 +463,7 @@ impl CodesearchService { ); let json = serde_json::to_string(&failure).unwrap_or_else(|_| failure.error.clone()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } None => {} } @@ -508,7 +508,7 @@ impl CodesearchService { let warnings = indexer.lookup_warnings(&db_path, &canonical); let impact = build_impact(references, Some(canonical), warnings); let json = serde_json::to_string(&impact).unwrap_or_else(|_| "{}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } ImpactLookupOutcome::Done(Err(e)) => { find_impact_tracker::IMPACT_LOOKUP_TRACKER.remove(&tracker_key); @@ -520,7 +520,7 @@ impl CodesearchService { ); let json = serde_json::to_string(&failure).unwrap_or_else(|_| failure.error.clone()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } ImpactLookupOutcome::Busy { state, waited_ms } => { tracing::warn!( @@ -541,7 +541,7 @@ impl CodesearchService { }; let json = serde_json::to_string(&busy).unwrap_or_else(|_| "{\"busy\":true}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } } } diff --git a/src/mcp/get_chunk.rs b/src/mcp/get_chunk.rs index d67ed0f6..8f4ffaed 100644 --- a/src/mcp/get_chunk.rs +++ b/src/mcp/get_chunk.rs @@ -5,7 +5,7 @@ use super::*; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; @@ -41,7 +41,7 @@ impl CodesearchService { if let Some(ref serve_state) = self.serve_state { let config = serve_state.config_snapshot(); if config.repos.len() > 1 { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( self.format_scope_error(), )])); } @@ -54,12 +54,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -112,7 +112,7 @@ impl CodesearchService { } match candidates.len() { 0 => { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result( format!( "Chunk {} not found in any repository. Verify the \ @@ -156,7 +156,7 @@ impl CodesearchService { &candidate_names, &chunk_warnings, ); - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( payload.to_string(), )])); } @@ -210,7 +210,7 @@ impl CodesearchService { let mut chunk = match chunk { Some(c) => c, None => { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result( format!( "Chunk {} not found. Verify the chunk_id and index state.", diff --git a/src/mcp/graph.rs b/src/mcp/graph.rs index 31cb320b..c1e5b190 100644 --- a/src/mcp/graph.rs +++ b/src/mcp/graph.rs @@ -3,7 +3,7 @@ //! `mod.rs` (todo #105). use super::*; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; impl CodesearchService { pub(crate) fn normalize_symbol_query_path(&self, project_root: &Path, file: &Path) -> PathBuf { @@ -26,12 +26,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -116,7 +116,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error reading imports: {e:#}" ))])); } @@ -263,12 +263,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -481,7 +481,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error resolving dependents: {e:#}" ))])); } @@ -510,12 +510,12 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -555,7 +555,7 @@ impl CodesearchService { let embedding = match embedding { Some(e) => e, None => { - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result( format!( "Embedding not found for chunk_id {} in any store.", @@ -657,7 +657,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error finding similar chunks: {e:#}" ))])); } diff --git a/src/mcp/literal_search.rs b/src/mcp/literal_search.rs index 9d49e639..b2edb2ce 100644 --- a/src/mcp/literal_search.rs +++ b/src/mcp/literal_search.rs @@ -3,7 +3,7 @@ //! `mod.rs` (todo #105). use super::*; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; impl CodesearchService { pub(crate) async fn literal_search( @@ -16,7 +16,7 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; let limit = request.limit.unwrap_or(20); @@ -51,7 +51,7 @@ impl CodesearchService { if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -194,7 +194,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error scanning chunks: {e:#}" ))])); } @@ -256,7 +256,7 @@ impl CodesearchService { { Ok(r) => r, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching: {e:#}" ))])); } @@ -405,7 +405,7 @@ impl CodesearchService { { Ok(items) => items, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error resolving search results: {e:#}" ))])); } @@ -494,6 +494,6 @@ impl CodesearchService { serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()) }; - Ok(CallToolResult::success(vec![Content::text(output)])) + Ok(CallToolResult::success(vec![ContentBlock::text(output)])) } } diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index f0eba479..66d79ef0 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -28,7 +28,7 @@ fn serve_url_from_env() -> String { } use crate::db_discovery::{find_best_database, load_repos_config}; -use crate::embed::{EmbeddingService, ModelType}; +use crate::embed::{EmbeddingServicePool, ModelType}; use crate::file::Language; use crate::fts::FtsStore; use crate::index::SharedStores; @@ -41,7 +41,7 @@ use regex::Regex; use rmcp::{ handler::server::router::tool::ToolRouter, handler::server::wrapper::Parameters, - model::{CallToolResult, Content, Implementation, ServerCapabilities, ServerInfo}, + model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo}, tool_handler, tool_router, ErrorData as McpError, ServerHandler, }; use std::collections::HashSet; @@ -148,8 +148,10 @@ pub struct CodesearchService { project_path: PathBuf, model_type: ModelType, dimensions: usize, - // Lazily initialized on first search - embedding_service: Arc>>, + // Lazily initialized on first search. A per-model pool: serve mode is + // multi-repo and each index records the model it was built with, so the + // query model is resolved per target repo (see `query_model`). + embedding_pool: Arc, // Shared stores for concurrent access (optional - only set when running with IndexManager) shared_stores: Option>, // Serve-mode state (set when running inside `codesearch serve`) @@ -194,6 +196,18 @@ impl Drop for CodesearchService { } } +/// Outcome of resolving the embedding model for a query target. +/// +/// See [`CodesearchService::resolve_query_model`]. +pub(crate) struct QueryModel { + /// The model the query must be embedded with. + pub model: ModelType, + /// A caller-facing warning, set when the target index records no model and + /// the built-in default was assumed. `None` when the model was recorded or + /// the query is scope-free. + pub assumed_warning: Option, +} + // v1: supports prefix/suffix patterns with `*` and `**` only. /// Merge exact FTS results into the main result set, deduplicating by chunk_id /// and keeping the max score for duplicates. @@ -828,7 +842,7 @@ pub(crate) fn allow_vector_store_second_open(has_shared_stores: bool) -> bool { // === Tool Router Implementation === -#[tool_router] +#[tool_router(allow_empty)] // ctors only; real tools merge in via merged_tool_router() impl CodesearchService { /// Create a new CodesearchService (standalone mode - opens its own VectorStore) #[allow(dead_code)] // Reserved for standalone MCP server mode @@ -884,7 +898,9 @@ impl CodesearchService { project_path, model_type, dimensions, - embedding_service: Arc::new(Mutex::new(None)), + embedding_pool: Arc::new(EmbeddingServicePool::new( + crate::constants::get_global_models_cache_dir().ok(), + )), shared_stores, serve_state: None, symbol_registry: Arc::new(SymbolIndexerRegistry::new()), @@ -898,13 +914,19 @@ impl CodesearchService { /// it routes requests to the repo identified by `project`/`group`. pub(crate) fn new_for_serve(serve_state: Arc) -> Result { let symbol_registry = serve_state.symbol_registry(); + // Seed the service with the serve-wide default model (`serve --model`), + // the same value `POST /repos` stamps into a newly created index, so the + // scope-free status summary reports it. It is deliberately NOT the query + // fallback for a repo whose metadata records no model — that resolves to + // the built-in default, with a warning. See `resolve_query_model`. + let model_type = serve_state.default_model().unwrap_or_default(); Ok(Self { tool_router: Self::merged_tool_router(), db_path: PathBuf::from("serve://multi-repo"), project_path: PathBuf::from("serve://multi-repo"), - model_type: ModelType::default(), - dimensions: crate::constants::DEFAULT_EMBEDDING_DIMENSIONS, - embedding_service: serve_state.embedding_service(), + model_type, + dimensions: model_type.dimensions(), + embedding_pool: serve_state.embedding_pool(), shared_stores: None, serve_state: Some(serve_state), symbol_registry, @@ -923,17 +945,71 @@ impl CodesearchService { self.tracks_session = true; } - /// Get or initialize the embedding service - fn get_embedding_service(&self) -> Result>> { - let mut guard = self.embedding_service.lock().unwrap(); - if guard.is_none() { - let cache_dir = crate::constants::get_global_models_cache_dir()?; - *guard = Some(EmbeddingService::with_cache_dir( - self.model_type, - Some(&cache_dir), - )?); + /// Resolve the embedding model a query against `alias` must use. + /// + /// With a repo alias (`project=` / group member) the model is read from that + /// repo's index metadata — an index built with EmbeddingGemma must be + /// queried with EmbeddingGemma, not the 384-dim default. A repo whose + /// metadata records no model is queried with the built-in default, never the + /// serve-wide `--model` default (see [`Self::resolve_query_model`]). With no + /// alias this returns the service's own `model_type`: the local index + /// metadata in stdio mode, the serve default in serve mode (the scope-free + /// status summary). Prefer [`Self::resolve_query_model`] when the caller can + /// surface the unrecorded-model warning. + pub(crate) fn query_model(&self, alias: Option<&str>) -> ModelType { + self.resolve_query_model(alias).model + } + + /// Resolve the query model together with a warning when it had to be assumed. + /// + /// The model a query is embedded with must match the model the target index + /// was built with. When `alias`'s metadata records no model the model is + /// unknowable, so this assumes the BUILT-IN default: that is both the + /// historical 384-dim behaviour and the value every other reader assumes for + /// metadata without a `model_short_name`. It deliberately does NOT assume the + /// serve-wide `--model` default: that flag selects the model for newly + /// created indexes, and using it here would break a working legacy repo the + /// moment an operator set it (a 384-dim index queried with a 768-dim model + /// fails, and a same-dimension model degrades rankings silently). The + /// returned warning names the repo, the assumption, and the re-index command. + pub(crate) fn resolve_query_model(&self, alias: Option<&str>) -> QueryModel { + if let (Some(state), Some(alias)) = (self.serve_state.as_ref(), alias) { + if let Some(model) = state.model_for_alias(alias) { + return QueryModel { + model, + assumed_warning: None, + }; + } + let model = ModelType::default(); + let warning = format!( + "repo '{alias}' records no embedding model; queried with the built-in default '{}' ({} dims). If this repo was indexed with a different model, re-index it: codesearch index --force --model ", + model.short_name(), + model.dimensions() + ); + if state.mark_legacy_model_warned(alias) { + tracing::warn!("{}", warning); + } + return QueryModel { + model, + assumed_warning: Some(warning), + }; + } + QueryModel { + model: self.model_type, + assumed_warning: None, } - Ok(guard) + } + + /// Get (lazily initializing) the embedding service for `model`. + /// + /// The returned `Arc>` is per-model, so concurrent queries against + /// different models do not serialise on one global lock. Callers MUST pass + /// the model the target index was built with — see [`Self::query_model`]. + pub(crate) fn embedding_service_for( + &self, + model: ModelType, + ) -> Result>> { + self.embedding_pool.get(model) } /// Return the current MCP mode as a string for diagnostics. @@ -1247,8 +1323,11 @@ impl CodesearchService { /// Fan-out vector store read across multiple stores, merging results. /// - /// Runs `action` against each store and merges all results into a single vec, - /// deduplicating by (alias, chunk_id) (keeping highest score) and sorting by score descending. + /// Runs `action(alias, store)` against each store and merges all results into + /// a single vec, deduplicating by (alias, chunk_id) (keeping highest score) + /// and sorting by score descending. The `alias` is passed to the closure so + /// callers can select per-repo state — notably the query embedding for that + /// repo's own model (see `semantic_search_multi`). /// /// A per-store failure does NOT abort the fan-out — one broken repo should /// not blind a group query to the healthy ones — but it is reported back in @@ -1261,7 +1340,7 @@ impl CodesearchService { aliases: &[String], ) -> Result> where - F: FnMut(&VectorStore) -> anyhow::Result>, + F: FnMut(&str, &VectorStore) -> anyhow::Result>, R: Clone + HasChunkId + HasScore, { let mut failures: Vec<(String, String)> = Vec::new(); @@ -1272,7 +1351,7 @@ impl CodesearchService { for (idx, store_arc) in stores.iter().enumerate() { let alias = aliases.get(idx).map(|s| s.as_str()).unwrap_or("unknown"); let store = store_arc.vector_store.read().await; - match action(&store) { + match action(alias, &store) { Ok(results) => { for r in results { let key = (alias.to_string(), r.chunk_id()); @@ -1494,7 +1573,7 @@ impl CodesearchService { self.literal_search(Parameters(req)).await? } _ => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown search mode '{}'. Use `semantic` or `literal`.", mode ))])); @@ -1666,7 +1745,7 @@ impl CodesearchService { let (peer_name, remote_alias, chunk_id) = match parse_federated_chunk_ref(chunk_ref) { Some(parts) => parts, None => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Invalid chunk_ref '{}': expected '/:'.", chunk_ref ))])); @@ -1677,7 +1756,7 @@ impl CodesearchService { Some(p) => p.clone(), None => { let known: Vec = cfg.remotes.keys().cloned().collect(); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown remote peer '{}' in chunk_ref '{}'. Known remotes: {}", peer_name, chunk_ref, @@ -1688,7 +1767,7 @@ impl CodesearchService { let client = match FederationClient::new() { Ok(c) => c, Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "federation disabled (http client error): {e}" ))])); } @@ -1703,11 +1782,11 @@ impl CodesearchService { .get_chunk(&peer, remote_alias, chunk_id, context_lines) .await { - Outcome::Ok(value) => Ok(CallToolResult::success(vec![Content::text( + Outcome::Ok(value) => Ok(CallToolResult::success(vec![ContentBlock::text( value.to_string(), )])), Outcome::Unreachable(reason) => { - Ok(CallToolResult::success(vec![Content::text(format!( + Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Could not fetch chunk from remote peer '{}': {}", peer_name, reason ))])) @@ -1739,7 +1818,7 @@ impl CodesearchService { }, }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - CallToolResult::success(vec![Content::text(json)]) + CallToolResult::success(vec![ContentBlock::text(json)]) } } @@ -1861,7 +1940,7 @@ type RestError = (StatusCode, AxumJson); /// Unwrap a `CallToolResult` into the JSON a federation client wants. /// -/// `CallToolResult` carries its payload as `Content::text(json_string)`. The +/// `CallToolResult` carries its payload as `ContentBlock::text(json_string)`. The /// normal case for search/find/explore/get_chunk is a single text item whose /// value parses as JSON, so we parse it back and return the structured value /// (clients get clean objects instead of a JSON-in-string). When the tool set diff --git a/src/mcp/proxy.rs b/src/mcp/proxy.rs index 38a9bb10..3a333d4b 100644 --- a/src/mcp/proxy.rs +++ b/src/mcp/proxy.rs @@ -1,6 +1,6 @@ use rmcp::{ model::{ - CallToolRequestParams, CallToolResult, Implementation, ListToolsResult, + CallToolRequestParams, CallToolResponse, Implementation, ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerInfo, }, service::RequestContext, @@ -425,7 +425,7 @@ impl ServerHandler for McpProxyService { &self, request: CallToolRequestParams, _cx: RequestContext, - ) -> Result { + ) -> Result { let _in_flight = InFlightGuard::new(&self.in_flight); let mut last_err: Option = None; for attempt in 0..PROXY_MAX_RETRY_ATTEMPTS { @@ -434,7 +434,7 @@ impl ServerHandler for McpProxyService { Some(p) => match p.call_tool(request.clone()).await { Ok(r) => { self.mark_activity(); - return Ok(r); + return Ok(r.into()); } Err(e) => { let msg = e.to_string(); diff --git a/src/mcp/responses.rs b/src/mcp/responses.rs index 9c670e60..4507fef3 100644 --- a/src/mcp/responses.rs +++ b/src/mcp/responses.rs @@ -2,7 +2,7 @@ use super::helpers::prefix_path_with_alias; use crate::embed::ModelType; use crate::index::SharedStores; use crate::vectordb::VectorStore; -use rmcp::model::{CallToolResult, Content}; +use rmcp::model::{CallToolResult, ContentBlock}; use rmcp::ErrorData as McpError; use std::path::Path; use std::sync::Arc; @@ -181,6 +181,7 @@ pub(crate) fn index_status_summary( total_repos: usize, failed_count: usize, total_chunks: usize, + all_indexed: bool, ) -> (String, String) { if total_repos > 0 && failed_count >= total_repos { ( @@ -196,6 +197,17 @@ pub(crate) fn index_status_summary( "Index is being built across {total_repos} repo(s). Searches may fail until indexing completes." ), ) + } else if !all_indexed { + // Chunks are in the stores but the HNSW vector index is not built yet + // (`VectorStore::search` refuses without it). Reporting "ready" here — + // the old behaviour, which keyed only off `total_chunks` — told an + // operator a rebuild was finished while every search still failed. + ( + "building".to_string(), + format!( + "Chunks are indexed across {total_repos} repo(s) but the vector index is not built yet — searches may fail until indexing completes." + ), + ) } else if failed_count > 0 { ( "ready".to_string(), @@ -212,6 +224,32 @@ pub(crate) fn index_status_summary( } } +/// Status/message for a single routed store's index. +/// +/// `indexed` (the HNSW graph is built and committed) is load-bearing: a store +/// with chunks but no built graph is NOT searchable — `VectorStore::search` +/// fails with "Index not built" — so it must not read as `ready`. During a +/// rebuild there is a window where chunks are inserted but `build_index()` has +/// not run yet, which is exactly when an operator asks "is the migration done?". +pub(crate) fn single_index_status(total_chunks: usize, indexed: bool) -> (String, String) { + if total_chunks == 0 { + ( + "building".to_string(), + "Index is being built in the background. Searches may fail until indexing completes. Please check back in a few minutes.".to_string(), + ) + } else if !indexed { + ( + "building".to_string(), + "Chunks are indexed but the vector index is not built yet — searches may fail until indexing completes. Please check back in a few minutes.".to_string(), + ) + } else { + ( + "ready".to_string(), + "Index is ready for searching.".to_string(), + ) + } +} + /// Turn a store's `stats()` result into the `(total_chunks, total_files, /// error)` triple `list_projects` reports per repo. /// @@ -333,13 +371,13 @@ pub(crate) fn respond_with_items_noted( message.push(' '); message.push_str(note); } - return Ok(CallToolResult::success(vec![Content::text( + return Ok(CallToolResult::success(vec![ContentBlock::text( qualify_empty_result(message, warnings), )])); } if note.is_none() && warnings.is_empty() { let json = serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } let mut payload = serde_json::Map::new(); payload.insert("results".to_string(), serde_json::json!(items)); @@ -349,7 +387,7 @@ pub(crate) fn respond_with_items_noted( if !warnings.is_empty() { payload.insert("warnings".to_string(), serde_json::json!(warnings)); } - Ok(CallToolResult::success(vec![Content::text( + Ok(CallToolResult::success(vec![ContentBlock::text( serde_json::Value::Object(payload).to_string(), )])) } @@ -385,12 +423,14 @@ pub(crate) fn respond_with_object( if let Ok(mut v) = serde_json::to_value(value) { if let Some(obj) = v.as_object_mut() { obj.insert("warnings".to_string(), serde_json::json!(warnings)); - return Ok(CallToolResult::success(vec![Content::text(v.to_string())])); + return Ok(CallToolResult::success(vec![ContentBlock::text( + v.to_string(), + )])); } } } let json = serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } /// Build the `ambiguous_chunk_id` payload for `get_chunk`. diff --git a/src/mcp/search.rs b/src/mcp/search.rs index bf903ee4..a00499a9 100644 --- a/src/mcp/search.rs +++ b/src/mcp/search.rs @@ -6,7 +6,7 @@ use super::*; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; @@ -94,7 +94,7 @@ impl CodesearchService { }; self.literal_search(Parameters(literal_req)).await } - _ => Ok(CallToolResult::success(vec![Content::text(format!( + _ => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown search mode '{}'. Use `semantic` or `literal`.", mode ))])), @@ -115,7 +115,7 @@ impl CodesearchService { .await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; let limit = request.limit.unwrap_or(10); @@ -136,7 +136,7 @@ impl CodesearchService { // Ensure database exists (skip if serve-mode with routed stores) if ctx.needs_local_db { if let Err(e) = self.ensure_database_exists() { - return Ok(CallToolResult::success(vec![Content::text(e)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(e)])); } } @@ -172,24 +172,34 @@ impl CodesearchService { } // === Modes: "semantic", "hybrid", "auto" — require embedding === + // The query MUST be embedded with the model the target index was built + // with. In serve mode that is the routed repo's recorded model, not a + // hub-wide default: a 384-dim query against a 768-dim EmbeddingGemma + // index failed with "expected 768, got 384". A repo that records no + // model is queried with the built-in default, and the caller is warned. + let model_resolution = self.resolve_query_model(ctx.project_alias.as_deref()); let query_embedding = { - let mut service_guard = match self.get_embedding_service() { - Ok(g) => g, + let model = model_resolution.model; + let service = match self.embedding_service_for(model) { + Ok(s) => s, Err(e) => { tracing::error!("MCP: Failed to get embedding service: {:?}", e); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error initializing embedding service: {e:#}" ))])); } }; - let service = service_guard.as_mut().unwrap(); - tracing::debug!("MCP: Embedding query..."); + let mut service = service.lock().unwrap(); + tracing::debug!( + "MCP: Embedding query with model '{}'...", + model.short_name() + ); match service.embed_query(&request.query) { Ok(e) => e, Err(e) => { tracing::error!("MCP: Failed to embed query: {:?}", e); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error embedding query: {e:#}" ))])); } @@ -201,6 +211,11 @@ impl CodesearchService { // here, `project=` — the form an agent uses most — still reports // a broken store as an ordinary empty result. let mut single_warnings: Vec = Vec::new(); + // Surface the assumed-model warning even when the store read succeeds: + // mismatched vector spaces do not error, they just rank wrongly. + if let Some(warning) = model_resolution.assumed_warning { + single_warnings.push(warning); + } // Search vector store let vector_results = match self @@ -227,7 +242,7 @@ impl CodesearchService { // ("Error reading from project-routed vector store"), which // hides the actual fault and makes remote diagnosis guesswork. if mode == "semantic" { - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching vector store: {:#}", e ))])); @@ -567,32 +582,69 @@ impl CodesearchService { } // === Modes requiring embedding: "semantic", "hybrid", "auto" === - let query_embedding = { - let mut service_guard = match self.get_embedding_service() { - Ok(g) => g, - Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( - "Error initializing embedding service: {e:#}" - ))])); - } - }; - let service = service_guard.as_mut().unwrap(); - match service.embed_query(&request.query) { - Ok(e) => e, - Err(e) => { - return Ok(CallToolResult::success(vec![Content::text(format!( - "Error embedding query: {e:#}" - ))])); + // + // Each repo may have been indexed with a different model, so the query + // is embedded once per distinct model and every store is searched with + // the embedding of ITS model. Embedding all stores with one hub-wide + // default is what produced "Query embedding dimension mismatch: + // expected 768, got 384" on a mixed hub. + let mut embeddings_by_alias: std::collections::HashMap> = + std::collections::HashMap::with_capacity(aliases.len()); + // Assumed-model warnings, one per repo that records no model. Collected + // here and folded into `search_warnings` below so an agent sees the + // assumption alongside the results it applies to. + let mut model_warnings: Vec = Vec::new(); + { + let mut by_model: std::collections::HashMap> = + std::collections::HashMap::new(); + for alias in aliases { + let model_resolution = self.resolve_query_model(Some(alias)); + let model = model_resolution.model; + if let Some(warning) = model_resolution.assumed_warning { + model_warnings.push(warning); } + let embedding = match by_model.get(&model) { + Some(cached) => cached.clone(), + None => { + let service = match self.embedding_service_for(model) { + Ok(s) => s, + Err(e) => { + return Ok(CallToolResult::success(vec![ContentBlock::text( + format!( + "Error initializing embedding service for '{alias}': {e:#}" + ), + )])); + } + }; + let mut service = service.lock().unwrap(); + let embedding = match service.embed_query(&request.query) { + Ok(e) => e, + Err(e) => { + return Ok(CallToolResult::success(vec![ContentBlock::text( + format!("Error embedding query: {e:#}"), + )])); + } + }; + by_model.insert(model, embedding.clone()); + embedding + } + }; + embeddings_by_alias.insert(alias.clone(), embedding); } - }; + } - // Search vector stores across all repos + // Search vector stores across all repos, each with its own model's + // query embedding. let outcome = self .with_vector_store_read_multi( - |store| { + |alias, store| { + let embedding = embeddings_by_alias.get(alias).ok_or_else(|| { + anyhow::anyhow!( + "internal error: no query embedding resolved for repo '{alias}'" + ) + })?; store - .search(&query_embedding, limit * 5) + .search(embedding, limit * 5) .context("Error searching vector store") }, stores.clone(), @@ -602,7 +654,8 @@ impl CodesearchService { // Warnings raised by the fan-out, carried into the response so the // calling agent can tell "not in the corpus" from "that repo is down". - let mut search_warnings: Vec = Vec::new(); + // Seeded with any assumed-model warnings gathered while embedding. + let mut search_warnings: Vec = model_warnings; let vector_results = match outcome { @@ -625,7 +678,7 @@ impl CodesearchService { .map(|(alias, err)| format!(" - {alias}: {err}")) .collect::>() .join("\n"); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching vector store: {} of {} repo(s) in scope failed \ and none returned results:\n{}", o.failures.len(), @@ -641,7 +694,7 @@ impl CodesearchService { } Err(e) => { tracing::error!("MCP: vector fan-out failed: {:?}", e); - return Ok(CallToolResult::success(vec![Content::text(format!( + return Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Error searching vector store: {e:#}" ))])); } @@ -978,7 +1031,7 @@ impl CodesearchService { warnings, }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } // Pre-compute normalized project root for stripping absolute paths @@ -1051,7 +1104,7 @@ impl CodesearchService { }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } /// Resolve FTS results to SearchResult by looking up chunk metadata. diff --git a/src/mcp/status.rs b/src/mcp/status.rs index 83695428..969f3d4c 100644 --- a/src/mcp/status.rs +++ b/src/mcp/status.rs @@ -6,7 +6,7 @@ use super::*; use rmcp::{ handler::server::wrapper::Parameters, - model::{CallToolResult, Content}, + model::{CallToolResult, ContentBlock}, tool, tool_router, ErrorData as McpError, }; @@ -25,13 +25,36 @@ impl CodesearchService { match kind.as_str() { "index" => self.index_status_impl(request.project, request.group).await, "projects" => self.list_projects().await, - _ => Ok(CallToolResult::success(vec![Content::text(format!( + _ => Ok(CallToolResult::success(vec![ContentBlock::text(format!( "Unknown status kind '{}'. Use `index` or `projects`.", kind ))])), } } + /// Model label for a grouped index-status response: the common model when + /// every member agrees, `"mixed"` when a hub holds indexes built with + /// different models. + /// + /// The status `model` field used to be the service's own model — the + /// hardcoded default in serve mode — so every repo read as `minilm-l6-q` + /// even when indexed with EmbeddingGemma. See the serve query-model fix. + pub(crate) fn group_model_label(&self, aliases: &[String]) -> String { + let mut common: Option = None; + for alias in aliases { + let model = self.query_model(Some(alias)); + match common { + None => common = Some(model), + Some(prev) if prev != model => return "mixed".to_string(), + _ => {} + } + } + common + .unwrap_or_else(|| self.query_model(None)) + .short_name() + .to_string() + } + // ───────────────────────────────────────────────────────────────── /// Internal implementation for index_status with optional project/group routing. async fn index_status_impl( @@ -92,14 +115,14 @@ impl CodesearchService { }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } } // Resolve project/group routing — status is scope-free, allow unscoped fan-out let ctx = match self.resolve_routing(&project, &group, true, "status").await { Ok(c) => c, - Err(e) => return Ok(CallToolResult::success(vec![Content::text(e)])), + Err(e) => return Ok(CallToolResult::success(vec![ContentBlock::text(e)])), }; if ctx.needs_local_db { @@ -121,7 +144,7 @@ impl CodesearchService { mode: self.mcp_mode(), }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } } @@ -132,6 +155,11 @@ impl CodesearchService { let mut max_chunk_id = 0u32; let mut dimensions = 0usize; let mut all_indexed = true; + // Separate from `all_indexed`: a store that FAILED to report stats + // must not make the summary claim "not built" (the failure already + // rides the `warnings` channel). This tracks only the graph state of + // stores that answered. + let mut all_built = true; let aliases = ctx.aliases(); let mut stats_warnings: Vec = Vec::new(); let mut failed_count = 0usize; @@ -150,6 +178,7 @@ impl CodesearchService { } if !stats.indexed { all_indexed = false; + all_built = false; } } // `all_indexed = false` alone renders identically to "still @@ -166,7 +195,7 @@ impl CodesearchService { } let (status, status_message) = - index_status_summary(sv.len(), failed_count, total_chunks); + index_status_summary(sv.len(), failed_count, total_chunks, all_built); let response = IndexStatusResponse { indexed: all_indexed, @@ -174,7 +203,7 @@ impl CodesearchService { status_message, total_chunks, total_files, - model: self.model_type.short_name().to_string(), + model: self.group_model_label(ctx.aliases()), dimensions, max_chunk_id, db_path: format!("({} repos)", sv.len()), @@ -202,7 +231,10 @@ impl CodesearchService { status_message: format!("{}", e), total_chunks: 0, total_files: 0, - model: self.model_type.short_name().to_string(), + model: self + .query_model(ctx.project_alias.as_deref()) + .short_name() + .to_string(), dimensions: 0, max_chunk_id: 0, db_path: self.db_path.display().to_string(), @@ -211,22 +243,13 @@ impl CodesearchService { mode: self.mcp_mode(), }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - return Ok(CallToolResult::success(vec![Content::text(json)])); + return Ok(CallToolResult::success(vec![ContentBlock::text(json)])); } }; - // Determine status based on database state - let (status, status_message) = if stats.total_chunks == 0 { - ( - "building".to_string(), - "Index is being built in the background. Searches may fail until indexing completes. Please check back in a few minutes.".to_string(), - ) - } else { - ( - "ready".to_string(), - "Index is ready for searching.".to_string(), - ) - }; + // Determine status based on database state. `stats.indexed` (the HNSW + // graph is built) is load-bearing — see `single_index_status`. + let (status, status_message) = single_index_status(stats.total_chunks, stats.indexed); let response = IndexStatusResponse { indexed: stats.indexed, @@ -234,7 +257,10 @@ impl CodesearchService { status_message, total_chunks: stats.total_chunks, total_files: stats.total_files, - model: self.model_type.short_name().to_string(), + model: self + .query_model(ctx.project_alias.as_deref()) + .short_name() + .to_string(), dimensions: stats.dimensions, max_chunk_id: stats.max_chunk_id, db_path: self.db_path.display().to_string(), @@ -244,7 +270,7 @@ impl CodesearchService { }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } /// List all registered projects and groups. Called by `status(kind="projects")`. @@ -426,6 +452,6 @@ impl CodesearchService { }; let json = serde_json::to_string(&response).unwrap_or_else(|_| "{}".to_string()); - Ok(CallToolResult::success(vec![Content::text(json)])) + Ok(CallToolResult::success(vec![ContentBlock::text(json)])) } } diff --git a/src/mcp/tests.rs b/src/mcp/tests.rs index 3490a4fa..3495b472 100644 --- a/src/mcp/tests.rs +++ b/src/mcp/tests.rs @@ -2044,14 +2044,14 @@ fn note_store_failure_survives_a_short_alias_list() { #[test] fn index_status_summary_reports_building_before_anything_failed() { - let (status, message) = super::index_status_summary(3, 0, 0); + let (status, message) = super::index_status_summary(3, 0, 0, true); assert_eq!(status, "building"); assert!(!message.contains("failed"), "got: {message}"); } #[test] fn index_status_summary_reports_clean_ready_with_no_failures() { - let (status, message) = super::index_status_summary(3, 0, 500); + let (status, message) = super::index_status_summary(3, 0, 500, true); assert_eq!(status, "ready"); assert!(!message.contains("failed"), "got: {message}"); assert!(message.contains("3 repo(s)"), "got: {message}"); @@ -2061,7 +2061,7 @@ fn index_status_summary_reports_clean_ready_with_no_failures() { fn index_status_summary_surfaces_a_degraded_group_as_ready_with_a_count() { // This is the exact case that used to be indistinguishable from // "index still warming": some data is in, one store didn't answer. - let (status, message) = super::index_status_summary(3, 1, 500); + let (status, message) = super::index_status_summary(3, 1, 500, true); assert_eq!( status, "ready", "the two healthy stores must not be masked by the one that failed" @@ -2081,7 +2081,7 @@ fn index_status_summary_reports_error_when_every_store_failed() { // this fix, `total_chunks == 0` was checked first and this rendered as // "building" — byte-identical to "not indexed yet" — even though every // store actively failed. `failed_count >= total_repos` must win. - let (status, message) = super::index_status_summary(3, 3, 0); + let (status, message) = super::index_status_summary(3, 3, 0, true); assert_eq!( status, "error", "a group where every store failed must not read as merely 'still building'" @@ -2236,10 +2236,10 @@ fn qualify_empty_result_contradicts_a_not_found_diagnosis() { #[test] fn respond_with_items_carries_warnings_on_every_path() { - use rmcp::model::RawContent; + use rmcp::model::ContentBlock; let text = |r: Result| -> String { - match &r.unwrap().content[0].raw { - RawContent::Text(t) => t.text.clone(), + match &r.unwrap().content[0] { + ContentBlock::Text(t) => t.text.clone(), other => panic!("expected text content, got {other:?}"), } }; @@ -2276,10 +2276,10 @@ fn respond_with_items_carries_warnings_on_every_path() { #[test] fn respond_with_items_noted_shapes_on_every_path() { - use rmcp::model::RawContent; + use rmcp::model::ContentBlock; let text = |r: Result| -> String { - match &r.unwrap().content[0].raw { - RawContent::Text(t) => t.text.clone(), + match &r.unwrap().content[0] { + ContentBlock::Text(t) => t.text.clone(), other => panic!("expected text content, got {other:?}"), } }; @@ -2427,10 +2427,10 @@ fn ambiguous_chunk_payload_is_unchanged_when_every_store_answered() { #[test] fn respond_with_object_carries_warnings_without_disturbing_the_healthy_shape() { - use rmcp::model::RawContent; + use rmcp::model::ContentBlock; let text = |r: Result| -> String { - match &r.unwrap().content[0].raw { - RawContent::Text(t) => t.text.clone(), + match &r.unwrap().content[0] { + ContentBlock::Text(t) => t.text.clone(), other => panic!("expected text content, got {other:?}"), } }; @@ -2765,3 +2765,46 @@ fn readonly_or_require_ready_never_creates_a_missing_index() { assert!(!super::may_create_missing_index(true, true, false)); assert!(!super::may_create_missing_index(true, false, true)); } + +/// Chunks without a built vector index are NOT searchable, and must not read as +/// `ready`. Regression guard for a rebuild window (and the mixed-state hub the +/// model migration exposed): `total_chunks > 0` used to be sufficient for +/// "ready", so `status` said "Index is ready for searching" while every search +/// failed with "Index not built. Call build_index() after inserting chunks." +#[test] +fn index_status_summary_reports_building_when_chunks_exist_but_graph_is_not_built() { + let (status, message) = super::index_status_summary(3, 0, 500, false); + assert_eq!( + status, "building", + "chunks without a built vector index are not searchable" + ); + assert!(message.contains("not built"), "got: {message}"); + assert!( + message.contains("3 repo(s)"), + "message must still name the scope, got: {message}" + ); + // A stats() failure on some stores must NOT be misread as "not built". + let (status, _) = super::index_status_summary(3, 1, 500, true); + assert_eq!( + status, "ready", + "a failed store is surfaced as degraded-ready" + ); +} + +/// Single-store counterpart: the same `total_chunks > 0` shortcut reported +/// `ready` for a store whose graph was never built (`indexed == false`). +#[test] +fn single_index_status_requires_a_built_graph_to_report_ready() { + let (status, message) = super::single_index_status(403, false); + assert_eq!( + status, "building", + "403 chunks with no built graph are not searchable" + ); + assert!(message.contains("not built"), "got: {message}"); + + let (status, _) = super::single_index_status(403, true); + assert_eq!(status, "ready"); + + let (status, _) = super::single_index_status(0, false); + assert_eq!(status, "building"); +} diff --git a/src/serve/mod.rs b/src/serve/mod.rs index 1bba9d93..8bca5806 100644 --- a/src/serve/mod.rs +++ b/src/serve/mod.rs @@ -3,8 +3,8 @@ //! Binds on `{host}:{port}` (default `127.0.0.1:39725`) and serves: //! - `GET /health` → JSON health check //! - `POST /repos` → register + index + warmup a new repo -//! - `DELETE /repos/:alias` → stop FSW + evict + unregister + delete DB -//! - `POST /repos/:alias/reindex` → trigger incremental or force reindex +//! - `DELETE /repos/{alias}` → stop FSW + evict + unregister + delete DB +//! - `POST /repos/{alias}/reindex` → trigger incremental or force reindex //! - MCP streamable HTTP at `/mcp` via rmcp tower service //! //! Holds a `DashMap>` keyed by repo alias. @@ -230,6 +230,18 @@ pub(crate) struct ServeState { /// drop first. The token is stored alongside the handle so `remove_repo` /// can cancel regardless of the repo's `RepoState` variant. index_tasks: DashMap, CancellationToken)>, + /// Aliases detected with LMDB storage-format corruption (e.g. + /// `MDB_BAD_VALSIZE` after a storage-layer major upgrade such as + /// arroy 0.5→0.8 / heed 0.20→0.22), queued for a wipe + full force + /// reindex. Processed strictly one at a time by the recovery worker — + /// each rebuild runs a full CPU-bound embed pass, so parallel + /// recoveries would thrash the machine. See + /// [`Self::enqueue_format_recovery`] / [`Self::recover_repo_format`]. + format_recovery_queue: std::sync::Mutex>, + /// Guarantees at most one recovery worker is alive. A worker that finds + /// the queue empty flips this back to `false` under the queue lock, so an + /// enqueue racing the worker's exit re-spawns cleanly (no lost wake-up). + format_recovery_worker_started: std::sync::atomic::AtomicBool, /// Loaded repos config (alias → path). config: std::sync::RwLock, /// Last observed mtime of the repos config file. @@ -269,12 +281,33 @@ pub(crate) struct ServeState { /// `find_impact` to reuse helper-detection cache instead of creating fresh /// instances per request. symbol_registry: Arc, - /// Shared embedding service — used by MCP sessions AND the REST handlers so - /// the ONNX embedding model is loaded ONCE per serve instance (lazily, on - /// the first semantic query) and reused across all requests. Without this, - /// per-request `CodesearchService` construction (REST handlers) would reload - /// the model on every call (~100ms–2s). Mirrors the `symbol_registry` pattern. - embedding_service: Arc>>, + /// Shared, per-model embedding-service pool — used by MCP sessions AND the + /// REST handlers so each ONNX embedding model is loaded ONCE per serve + /// instance (lazily, on the first semantic query) and reused across all + /// requests. Without this, per-request `CodesearchService` construction + /// (REST handlers) would reload the model on every call (~100ms–2s). + /// + /// A pool rather than a single service because serve is multi-repo and + /// indexes may be built with different models: every query must be embedded + /// with the model of the repo it targets. Mirrors the `symbol_registry` + /// pattern. + embedding_pool: Arc, + /// Serve-wide default embedding model for newly created indexes + /// (`codesearch serve --model `), or `None` for the built-in default. + /// + /// This never overrides an index that already records its own model, and it + /// is deliberately NOT the query fallback for an index that records none: + /// a legacy index with no `model_short_name` is queried with the built-in + /// default and reported with a warning (see + /// `CodesearchService::resolve_query_model`). Applying this flag there would + /// break a working legacy repo the moment an operator set it. The default + /// applies only when `POST /repos` creates a brand-new index without an + /// explicit `model`, and to the scope-free status summary. + default_model: Option, + /// Aliases for which the unrecorded-model query warning has already been + /// emitted, so a long-running serve logs it once per repo instead of once + /// per query. See [`Self::mark_legacy_model_warned`]. + legacy_model_warned: DashMap, /// Per-repo total tool call count. tool_call_counts: DashMap, /// Per-repo C# symbol index status (cached, updated on rebuild/detect). @@ -343,6 +376,8 @@ impl ServeState { open_locks: DashMap::new(), fsw_tasks: DashMap::new(), index_tasks: DashMap::new(), + format_recovery_queue: std::sync::Mutex::new(std::collections::VecDeque::new()), + format_recovery_worker_started: std::sync::atomic::AtomicBool::new(false), config: std::sync::RwLock::new(config), config_mtime: std::sync::RwLock::new(None), config_path_override, @@ -354,7 +389,11 @@ impl ServeState { total_sessions: AtomicU64::new(0), sysinfo_system: std::sync::Mutex::new(sys), symbol_registry: Arc::new(SymbolIndexerRegistry::new()), - embedding_service: Arc::new(std::sync::Mutex::new(None)), + embedding_pool: Arc::new(crate::embed::EmbeddingServicePool::new( + crate::constants::get_global_models_cache_dir().ok(), + )), + default_model: None, + legacy_model_warned: DashMap::new(), tool_call_counts: DashMap::new(), csharp_index_status: Arc::new(DashMap::new()), csharp_index_error: Arc::new(DashMap::new()), @@ -447,14 +486,56 @@ impl ServeState { Arc::clone(&self.symbol_registry) } - /// Return a clone of the shared embedding-service Arc. - /// Shared across MCP sessions AND REST handlers so the ONNX model is loaded + /// Return a clone of the shared, per-model embedding-service pool. + /// Shared across MCP sessions AND REST handlers so each ONNX model is loaded /// once per serve instance (lazily on first semantic query) instead of being /// reloaded per request/session. - pub(crate) fn embedding_service( - &self, - ) -> Arc>> { - Arc::clone(&self.embedding_service) + pub(crate) fn embedding_pool(&self) -> Arc { + Arc::clone(&self.embedding_pool) + } + + /// Attach the serve-wide default embedding model (`codesearch serve --model`). + /// + /// Set once at startup, before the state is shared. `None` leaves the + /// built-in default in place. + pub(crate) fn with_default_model(mut self, model: Option) -> Self { + self.default_model = model; + self + } + + /// The serve-wide default embedding model for newly created indexes, or + /// `None` for the built-in default. See [`Self::with_default_model`]. + pub(crate) fn default_model(&self) -> Option { + self.default_model + } + + /// Resolve the embedding model an alias's index was built with. + /// + /// Returns `None` when the alias is unknown or its index has no + /// `model_short_name` (unindexed / legacy), so callers can fall back to + /// [`crate::embed::ModelType::default`]. This is the read side of the + /// per-repo model contract: a query against `alias` MUST be embedded with + /// the model returned here, or the vector search fails with a dimension + /// mismatch (768-dim EmbeddingGemma index, 384-dim default query) or + /// silently compares incomparable vector spaces. + pub(crate) fn model_for_alias(&self, alias: &str) -> Option { + let cfg = self.config_snapshot(); + let project_path = cfg.resolve(alias)?; + crate::embed::ModelType::from_index_metadata(&project_path.join(DB_DIR_NAME)) + } + + /// Record that `alias` was queried with the built-in default because its + /// index records no embedding model, returning `true` on the first call for + /// that alias. + /// + /// An unrecorded model is unknowable, so the fallback warning is logged once + /// per repo per serve lifetime rather than on every query — a busy hub would + /// otherwise flood the log with the same line. The caller-facing response + /// warning is not deduped: an agent should see the assumption on each answer. + pub(crate) fn mark_legacy_model_warned(&self, alias: &str) -> bool { + self.legacy_model_warned + .insert(alias.to_string(), ()) + .is_none() } /// Return the instant when serve started, used to compute uptime. @@ -546,6 +627,182 @@ impl ServeState { self.active_reindexes.remove(alias); } + /// True iff an error chain indicates LMDB storage-format corruption — + /// data written by an older storage-major (arroy/heed) that the current + /// one refuses to read — rather than a transient or unrelated failure. + fn is_lmdb_format_corruption(msg: &str) -> bool { + let m = msg.to_ascii_lowercase(); + m.contains("mdb_bad_valsize") + || m.contains("unsupported size of key") + || m.contains("wrong dupfixed size") + } + + /// Queue `alias` for a sequential wipe + force reindex after LMDB format + /// corruption was detected. Deduplicates; spawns the single recovery + /// worker on the first enqueue. + fn enqueue_format_recovery(self: &Arc, alias: &str) { + { + let mut queue = self + .format_recovery_queue + .lock() + .expect("format_recovery_queue lock poisoned"); + if queue.iter().any(|a| a == alias) { + return; + } + queue.push_back(alias.to_string()); + } + // Swap AFTER the push so the worker-exit path (which flips the flag + // back to `false` while still holding the queue lock) can never race + // us into a lost wake-up: either we observe `true` and the live + // worker picks up the fresh entry, or we flip `false→true` and spawn. + if !self + .format_recovery_worker_started + .swap(true, std::sync::atomic::Ordering::AcqRel) + { + let state = Arc::clone(self); + tokio::spawn(state.format_recovery_worker()); + } + } + + /// Pops queued aliases and recovers them ONE AT A TIME until the queue + /// runs dry, then exits (a later enqueue restarts a worker). + async fn format_recovery_worker(self: Arc) { + loop { + let alias = { + let mut queue = self + .format_recovery_queue + .lock() + .expect("format_recovery_queue lock poisoned"); + match queue.pop_front() { + Some(a) => a, + None => { + // Flip the flag while still holding the queue lock so + // a concurrent enqueue cannot interleave between the + // empty pop and the flag reset (lost wake-up). + self.format_recovery_worker_started + .store(false, std::sync::atomic::Ordering::Release); + return; + } + } + }; + if let Err(e) = self.recover_repo_format(&alias).await { + tracing::error!("🔧 Format recovery failed for '{}': {}", alias, e); + } + } + } + + /// Wipe + force reindex one repo whose on-disk storage was written by an + /// older storage-major. Mirrors `remove_repo`'s eviction sequence (stop + /// FSW → evict → await watcher/index shutdowns) but keeps the alias + /// registered; then deletes the DB directory (bounded retry for transient + /// Windows lock holders) and reuses the TUI force-reindex machinery — its + /// `try_open_stores` path recreates fresh stores when the directory is + /// gone, so the rebuild lands on the new arroy/heed formats. + async fn recover_repo_format(self: &Arc, alias: &str) -> Result<(), String> { + let project_path = { + let config = self + .config + .read() + .map_err(|_| "config lock poisoned".to_string())?; + if config.repo_read_only.get(alias) == Some(&true) { + return Err(format!( + "'{}' is marked read-only; rebuild its index on the owning writer", + alias + )); + } + config + .resolve(alias) + .ok_or_else(|| format!("unknown alias '{}'", alias))? + }; + let db_path = project_path.join(DB_DIR_NAME); + + // Evict in-memory holders so the LMDB env closes before the delete + // (Windows refuses to delete mmap'd files). Same order as remove_repo. + { + let _stores = self.stop_fsw(alias); + } + self.repos.remove(alias); + self.last_access.remove(alias); + self.await_fsw_shutdown(alias).await; + self.await_index_task(alias).await; + + let deadline = + Instant::now() + Duration::from_secs(crate::constants::DB_DELETE_RETRY_BUDGET_SECS); + let mut backoff_ms = crate::constants::DB_DELETE_RETRY_INITIAL_MS; + loop { + match std::fs::remove_dir_all(&db_path) { + Ok(()) => break, + Err(e) if e.kind() == std::io::ErrorKind::NotFound || !db_path.exists() => break, + Err(e) if Self::is_db_locked_error(&e) && Instant::now() < deadline => { + tracing::debug!( + "Format recovery: DB dir for '{}' still locked, retrying: {}", + alias, + e + ); + tokio::time::sleep(Duration::from_millis(backoff_ms)).await; + backoff_ms = (backoff_ms * 2).min(2_000); + } + Err(e) => { + return Err(format!( + "could not wipe {} after corruption: {}", + db_path.display(), + e + )) + } + } + } + tracing::info!( + "🔧 Format recovery: wiped stale-format DB dir for '{}' — rebuilding", + alias + ); + + match tui::spawn_force_reindex(alias.to_string(), self) { + tui::ReindexLaunch::Started => { + // Sequential guarantee: wait until this alias stops indexing + // before the worker loop picks the next one. Poll — the + // active-reindexes entry can go stale (MAX_INDEXING_SECS) on + // very long rebuilds, so cap generously and surface a timeout + // rather than hanging the whole recovery queue. + let cap = self.indexing_timeout() * 8; + let started = Instant::now(); + while self.is_indexing(alias) { + if started.elapsed() >= cap { + return Err(format!( + "rebuild for '{}' exceeded the {}s recovery cap", + alias, + cap.as_secs() + )); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + tracing::info!("🔧 Format recovery: rebuild complete for '{}'", alias); + Ok(()) + } + tui::ReindexLaunch::AlreadyRunning => { + // A rebuild is already in flight for this alias; wait it out. + // If it was a plain rebuild it may fail on the corrupt dir + // again — the next rebuild trigger re-detects and re-queues. + let cap = self.indexing_timeout() * 8; + let started = Instant::now(); + while self.is_indexing(alias) { + if started.elapsed() >= cap { + return Err(format!( + "in-flight rebuild for '{}' exceeded the {}s recovery cap", + alias, + cap.as_secs() + )); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + Ok(()) + } + tui::ReindexLaunch::Failed => Err(format!( + "could not start the recovery rebuild for '{}' (see log)", + alias + )), + } + } + /// Returns `true` if `alias` is currently (non-stale) indexing. /// /// Stale entries — those older than [`MAX_INDEXING_SECS`] — are lazily @@ -1372,7 +1629,7 @@ impl ServeState { /// Remove a repo: stop FSW, evict from memory, unregister from config, delete DB. /// - /// This is the shared logic used by both the HTTP `DELETE /repos/:alias` handler + /// This is the shared logic used by both the HTTP `DELETE /repos/{alias}` handler /// and the TUI confirmation flow. pub(crate) async fn remove_repo(&self, alias: &str) -> Result { // 1. Resolve project path from config @@ -1995,7 +2252,7 @@ impl ServeState { let db_path = path.join(DB_DIR_NAME); // Open stores: existence check + write/readonly/conflicted logic. - let stores = match self.try_open_stores(alias, &db_path, false, force_readonly)? { + let stores = match self.try_open_stores(alias, &db_path, false, force_readonly, None)? { OpenedStores::Readonly(stores) => { // Already registered as Readonly by try_open_stores. // @@ -2197,7 +2454,7 @@ impl ServeState { let db_path = path.join(DB_DIR_NAME); // Open stores: existence check + write/readonly/conflicted logic. - let stores = match self.try_open_stores(alias, &db_path, false, force_readonly)? { + let stores = match self.try_open_stores(alias, &db_path, false, force_readonly, None)? { OpenedStores::Readonly(s) => { // Already registered as Readonly; touch and return. self.touch_access(alias); @@ -2443,12 +2700,20 @@ impl ServeState { /// /// `allow_create=false`: warmup / incremental reindex path — fails if DB is missing. /// `allow_create=true`: force-reindex / add-repo path — creates fresh DB if missing. + /// + /// `dimension_override` forces the embeddings dimension (e.g. a model + /// override on `POST /repos`); `None` reads it from `metadata.json`. The + /// caller must have made the on-disk store consistent with the override + /// (a fresh DB, or one whose data will be cleared by the reindex) — opening + /// a store at a different dimension than its vectors were written with + /// yields a dimension mismatch on the first insert. fn try_open_stores( &self, alias: &str, db_path: &Path, allow_create: bool, force_readonly: bool, + dimension_override: Option, ) -> std::result::Result { if !db_path.exists() && !allow_create { let parent = db_path @@ -2464,7 +2729,7 @@ impl ServeState { )); } - let dims = self.get_dimensions_for_path(db_path); + let dims = dimension_override.unwrap_or_else(|| self.get_dimensions_for_path(db_path)); // Read-only requested via the per-repo `repo_read_only` config flag: // open readonly directly and never attempt a write open. This makes @@ -3237,6 +3502,10 @@ async fn status_handler( let uptime_secs = state.started_at().elapsed().as_secs(); + // Serve-wide default model for newly created indexes (`serve --model`). + // `null` means the built-in default. + let default_model = state.default_model().map(|m| m.short_name()); + // CPU usage — reuse shared System instance so cpu_usage() can compute delta let cpu = { use sysinfo::ProcessesToUpdate; @@ -3247,6 +3516,7 @@ async fn status_handler( "version": env!("CARGO_PKG_VERSION"), "repos": repo_json, "active_sessions": active_sessions, + "default_model": default_model, "cpu_percent": "—", "uptime_secs": uptime_secs, })); @@ -3259,6 +3529,7 @@ async fn status_handler( "version": env!("CARGO_PKG_VERSION"), "repos": repo_json, "active_sessions": active_sessions, + "default_model": default_model, "cpu_percent": "—", "uptime_secs": uptime_secs, })); @@ -3291,6 +3562,7 @@ async fn status_handler( "version": env!("CARGO_PKG_VERSION"), "repos": repo_json, "active_sessions": active_sessions, + "default_model": default_model, "cpu_percent": cpu, "csharp_helper": csharp_helper, "ts_helper": ts_helper, @@ -3588,14 +3860,29 @@ async fn trigger_symbol_rebuild( state.schedule_persist_repos_config(); } Ok(Err(e)) => { - tracing::error!("❌ Symbol rebuild failed for '{}': {}", alias_owned, e); + let msg = e.to_string(); state.end_indexing(&alias_owned); state .csharp_index_error - .insert(alias_owned.clone(), e.to_string()); - state - .csharp_index_status - .insert(alias_owned, CSharpIndexStatus::Error); + .insert(alias_owned.clone(), msg.clone()); + if ServeState::is_lmdb_format_corruption(&msg) { + tracing::warn!( + "⚠️ LMDB storage-format corruption for '{}' (data written by an older \ + storage-major) — queueing sequential wipe + full rebuild", + alias_owned + ); + // Recovery owns the outcome from here: show in-progress rather + // than Error; it flips to Ready on success or Error on failure. + state + .csharp_index_status + .insert(alias_owned.clone(), CSharpIndexStatus::Indexing); + state.enqueue_format_recovery(&alias_owned); + } else { + tracing::error!("❌ Symbol rebuild failed for '{}': {}", alias_owned, msg); + state + .csharp_index_status + .insert(alias_owned, CSharpIndexStatus::Error); + } } Err(e) => { tracing::error!( @@ -3757,7 +4044,7 @@ async fn reindex_handler( // FSW not running -- open existing or create fresh DB. // allow_create=true so a force-reindex can recover a deleted DB. let cancel = CancellationToken::new(); - match state.try_open_stores(&alias, &db_path, true, false) { + match state.try_open_stores(&alias, &db_path, true, false, None) { Ok(OpenedStores::Write(s)) => { // Register as Write to block double-open races while we reindex. state.repos.insert( @@ -3969,6 +4256,29 @@ struct AddRepoRequest { model: Option, } +/// Decide the embedding model a `POST /repos` add should index with. +/// +/// Precedence: +/// 1. an explicit `model` in the request always wins (it forces a rebuild at +/// that model's dimension, which is the documented `index add --model` +/// behavior); +/// 2. otherwise the serve-wide default (`codesearch serve --model`) applies +/// **only when no model is recorded on disk** — i.e. this call is creating a +/// brand-new index; +/// 3. an index that already records its own model keeps it, exactly as if +/// `--model` had not been passed. +fn resolve_add_repo_model( + explicit: Option, + recorded: Option, + serve_default: Option, +) -> Option { + explicit.or(if recorded.is_none() { + serve_default + } else { + None + }) +} + /// Add-repo handler: POST /repos /// /// Registers a new repo in repos.json, opens the LMDB/Tantivy stores inline @@ -4010,6 +4320,42 @@ async fn add_repo_handler( ); } + // db_path is resolved before the model decision: whether a serve-wide + // default applies depends on whether the index already records a model. + let db_path = canonical_path.join(DB_DIR_NAME); + + // Parse the optional model override BEFORE opening the store: a fresh index + // must be created at the override's dimension, not the 384-dim default. + // Previously the store was opened at the default (or the previous metadata's) + // dimension and the override was only applied to metadata afterwards, so the + // reindex embedded 768-dim vectors into a 384-dim store and indexed nothing. + let explicit_model: Option = match body.model.as_deref() { + Some(model_str) => match crate::embed::ModelType::parse(model_str) { + Some(mt) => Some(mt), + None => { + return ( + StatusCode::BAD_REQUEST, + axum::response::Json(json!({ + "error": format!("Unknown model: '{}'. Use one of: {}", model_str, crate::embed::ModelType::valid_short_names()), + "status": "error" + })), + ); + } + }, + None => None, + }; + + // `codesearch serve --model X` sets the default for indexes created here. + // It applies only when no explicit `model` was given AND this POST is + // creating a brand-new index: an existing index keeps the model recorded in + // its `metadata.json`, exactly as if the flag had not been set. An explicit + // `model` still wins and rebuilds at that model's dimension. + let model_override = resolve_add_repo_model( + explicit_model, + crate::embed::ModelType::from_index_metadata(&db_path), + state.default_model(), + ); + // Register in repos.json let alias = { let mut config = match state.config.write() { @@ -4067,8 +4413,13 @@ async fn add_repo_handler( // This eliminates the LMDB double-open race that occurred when the old // path opened its own LMDB handle, conflicting with // calls from the serve's request handlers. - let db_path = canonical_path.join(DB_DIR_NAME); - let stores = match state.try_open_stores(&alias, &db_path, true, false) { + let stores = match state.try_open_stores( + &alias, + &db_path, + true, + false, + model_override.map(|m| m.dimensions()), + ) { Ok(OpenedStores::Write(s)) => s, Ok(OpenedStores::Readonly(_)) => { unreachable!( @@ -4139,23 +4490,6 @@ async fn add_repo_handler( ); } - // Parse optional model override from request body. - let model_override: Option = match body.model.as_deref() { - Some(model_str) => match crate::embed::ModelType::parse(model_str) { - Some(mt) => Some(mt), - None => { - return ( - StatusCode::BAD_REQUEST, - axum::response::Json(json!({ - "error": format!("Unknown model: '{}'. Use one of: {}", model_str, crate::embed::ModelType::valid_short_names()), - "status": "error" - })), - ); - } - }, - None => None, - }; - // Spawn the heavy indexing work in the background. Returns 202 immediately. let alias_bg = alias.clone(); let state_bg = state.clone(); @@ -4319,7 +4653,7 @@ pub(crate) struct RepoRemovalOutcome { pub db_delete_error: Option, } -/// Remove-repo handler: DELETE /repos/:alias +/// Remove-repo handler: DELETE /repos/{alias} /// /// Stops the FSW, evicts the repo from memory, unregisters from repos.json, /// and deletes the database directory. Returns 200 on success (status is @@ -4500,8 +4834,8 @@ fn request_has_valid_api_key(headers: &axum::http::HeaderMap, configured: &str) /// /// When the env var is unset or empty, all requests pass through (backward compatible). /// -/// Management endpoints are: `POST /repos`, `DELETE /repos/:alias`, -/// `POST /repos/:alias/reindex`, `POST /reload`. +/// Management endpoints are: `POST /repos`, `DELETE /repos/{alias}`, +/// `POST /repos/{alias}/reindex`, `POST /reload`. /// All other routes (health, status, MCP) are always unauthenticated. /// /// Key comparison is constant-time (see `api_key_matches`). @@ -4953,10 +5287,16 @@ fn keep_warm_foreign_target(ping_url: &str, self_host: &str) -> Option { } } +// `run_serve` is the single startup entry point, so its parameter list is the +// serve CLI surface (bind host/port, registration, default model, TUI, +// keep-warm, shutdown). Bundling them into a struct would only move the +// plumbing; allow the wide signature instead. +#[allow(clippy::too_many_arguments)] pub async fn run_serve( host: Option, port: Option, register_paths: Vec, + default_model: Option, no_tui: bool, keep_warm_url: Option, idle_suspend_secs: Option, @@ -5040,7 +5380,7 @@ pub async fn run_serve( // env > default); nothing else consumes it, so `ServeState` does not carry // it. In particular the embedded TUI must NOT derive a poll cadence from it // — it never polls a federated peer on a timer at all. - let serve_state = Arc::new(ServeState::new(config, None)); + let serve_state = Arc::new(ServeState::new(config, None).with_default_model(default_model)); // Construct the bind address from resolved host + port. // Using `format!` with `parse::()` handles both IPv4 and IPv6. @@ -5074,6 +5414,20 @@ pub async fn run_serve( info!("📋 Registered repos: {}", repo_list); eprintln!("📋 Registered repos: {}", repo_list); + // Report the serve-wide default model for newly created indexes, if set. + // Without this, `serve --model X` is a silent setting: the TUI/status show + // per-repo models, but nothing tells an operator what a new `POST /repos` + // (or a delegated `codesearch index add`) will use. + if let Some(model) = default_model { + let line = format!( + "🧠 Default model for new indexes: {} ({} dims)", + model.short_name(), + model.dimensions() + ); + info!("{}", line); + eprintln!("{}", line); + } + // ── Start HTTP server FIRST ── // Accept connections immediately so MCP clients don't time out. // Pre-warming runs in the background below. @@ -5136,24 +5490,24 @@ pub async fn run_serve( // /remotes is a status-like read-only observability endpoint (lists the // configured federation peers). It is NOT in require_admin_auth's // `is_management` set, so it inherits exactly the same auth policy as - // /status, /repos/:alias/info and /repos/:alias/doctor: reachable + // /status, /repos/{alias}/info and /repos/{alias}/doctor: reachable // without the admin key on localhost, protected by // require_auth_for_network on network binds. See REMOTES_PATH doc. .route(REMOTES_PATH, axum::routing::get(remotes_handler)) .route("/repos", axum::routing::post(add_repo_handler)) - .route("/repos/:alias", axum::routing::delete(remove_repo_handler)) + .route("/repos/{alias}", axum::routing::delete(remove_repo_handler)) .route("/reload", axum::routing::post(reload_handler)) .route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post(reindex_handler), ) - .route("/repos/:alias/info", axum::routing::get(info_handler)) + .route("/repos/{alias}/info", axum::routing::get(info_handler)) // /doctor is a POST but is intentionally read-only (diagnostics only, no // --fix path), so like /info and /status it is NOT in require_admin_auth's // management set — reachable without the admin key on localhost, and still // protected by require_auth_for_network on network binds. If doctor ever // gains a mutating mode, add it to `is_management` in require_admin_auth. - .route("/repos/:alias/doctor", axum::routing::post(doctor_handler)) + .route("/repos/{alias}/doctor", axum::routing::post(doctor_handler)) // REST endpoints — federation-friendly HTTP+JSON mirror of the read-only // MCP tools (search/find/explore/get_chunk). Lets a remote codesearch // serve be queried WITHOUT an MCP session. Same auth layers as /mcp & diff --git a/src/serve/tests.rs b/src/serve/tests.rs index b563d0ec..80a4d8fe 100644 --- a/src/serve/tests.rs +++ b/src/serve/tests.rs @@ -1,4 +1,5 @@ use super::*; +use serial_test::serial; use std::io::Write; #[test] @@ -769,7 +770,7 @@ async fn try_open_stores_creates_db_for_brand_new_repo() { let state = state_with_config(ReposConfig::default()); - match state.try_open_stores("brandnew", &db_path, true, false) { + match state.try_open_stores("brandnew", &db_path, true, false, None) { Ok(OpenedStores::Write(_)) => {} Ok(OpenedStores::Readonly(_)) => { panic!("brand-new repo opened Readonly; expected Write") @@ -796,8 +797,15 @@ async fn try_open_stores_creates_db_for_brand_new_repo() { /// handler's synchronous pre-spawn state — no embedding model required, no /// race. `persist_config` honors the temp config override, so the real /// `~/.codesearch/repos.json` is never touched. +/// +/// `#[serial]` + env reset: the handler reads `CODESEARCH_ALLOWED_ROOTS` via +/// `validate_path_within_allowed_roots`, so this test must not run while the +/// `allowed_roots_tests` below are mutating it (and must not inherit a stale +/// value from ambient state). +#[serial] #[tokio::test] async fn add_repo_handler_registers_brand_new_repo_without_rollback() { + let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let tmp = tempfile::tempdir().unwrap(); let repo_path = tmp.path().join("brandnew"); std::fs::create_dir(&repo_path).unwrap(); @@ -843,6 +851,250 @@ async fn add_repo_handler_registers_brand_new_repo_without_rollback() { ); } +/// `POST /repos` with no explicit `model` must create a brand-new index at the +/// serve-wide default's dimension (`codesearch serve --model X`), not the +/// built-in 384-dim default. This is the write-side counterpart of the per-repo +/// query-model contract. +#[tokio::test] +async fn add_repo_handler_uses_serve_default_model_for_new_index() { + let tmp = tempfile::tempdir().unwrap(); + let repo_path = tmp.path().join("defaulted"); + std::fs::create_dir(&repo_path).unwrap(); + + let state = Arc::new( + state_with_config(ReposConfig::default()) + .with_default_model(Some(crate::embed::ModelType::EmbeddingGemma300MQ4)), + ); + + let (status, body) = add_repo_handler( + axum::extract::State(state.clone()), + axum::extract::Json(AddRepoRequest { + path: repo_path.clone(), + alias: Some("defaulted".to_string()), + model: None, + }), + ) + .await; + assert_eq!( + status, + axum::http::StatusCode::ACCEPTED, + "add must be accepted, got {}: {}", + status, + body.0 + ); + + let stores = state + .get_opened_stores("defaulted") + .expect("store must be open immediately after add"); + let dims = stores + .vector_store + .try_read() + .unwrap() + .stats() + .unwrap() + .dimensions; + assert_eq!( + dims, + crate::embed::ModelType::EmbeddingGemma300MQ4.dimensions(), + "a new index must be created at the serve default model's dimension" + ); +} + +/// The serve-wide default must NOT override an index that already records its +/// own model: re-adding a repo whose `.codesearch.db` is still on disk keeps the +/// recorded model and dimension. +#[tokio::test] +async fn add_repo_handler_keeps_recorded_model_over_serve_default() { + let tmp = tempfile::tempdir().unwrap(); + let repo_path = tmp.path().join("existing"); + std::fs::create_dir(&repo_path).unwrap(); + let db_path = repo_path.join(DB_DIR_NAME); + + // A pre-existing index recording the 384-dim default model. + std::fs::create_dir_all(&db_path).unwrap(); + let mut meta = serde_json::Map::new(); + crate::embed::ModelType::AllMiniLML6V2Q.write_metadata_fields(&mut meta); + std::fs::write( + db_path.join("metadata.json"), + serde_json::to_string(&meta).unwrap(), + ) + .unwrap(); + + let state = Arc::new( + state_with_config(ReposConfig::default()) + .with_default_model(Some(crate::embed::ModelType::EmbeddingGemma300MQ4)), + ); + + let (status, body) = add_repo_handler( + axum::extract::State(state.clone()), + axum::extract::Json(AddRepoRequest { + path: repo_path.clone(), + alias: Some("existing".to_string()), + model: None, + }), + ) + .await; + assert_eq!( + status, + axum::http::StatusCode::ACCEPTED, + "add must be accepted, got {}: {}", + status, + body.0 + ); + + let stores = state + .get_opened_stores("existing") + .expect("store must be open immediately after add"); + let dims = stores + .vector_store + .try_read() + .unwrap() + .stats() + .unwrap() + .dimensions; + assert_eq!( + dims, + crate::embed::ModelType::AllMiniLML6V2Q.dimensions(), + "an existing index must keep its recorded model, not adopt the serve default" + ); +} + +/// Precedence contract for the model a `POST /repos` add indexes with. +#[test] +fn resolve_add_repo_model_precedence() { + use crate::embed::ModelType; + let gemma = ModelType::EmbeddingGemma300MQ4; + let mini = ModelType::AllMiniLML6V2Q; + + // Explicit model always wins, even over a recorded model and a default. + assert_eq!( + resolve_add_repo_model(Some(gemma), Some(mini), Some(mini)), + Some(gemma) + ); + // No explicit model, no recorded model → serve default applies (new index). + assert_eq!(resolve_add_repo_model(None, None, Some(gemma)), Some(gemma)); + // No explicit model, recorded model present → serve default is ignored. + assert_eq!(resolve_add_repo_model(None, Some(mini), Some(gemma)), None); + // No explicit model, no recorded model, no default → no override. + assert_eq!(resolve_add_repo_model(None, None, None), None); + // Explicit model still wins when nothing else is set. + assert_eq!(resolve_add_repo_model(Some(mini), None, None), Some(mini)); +} + +/// The serve-wide default is the scope-free fallback model in serve mode (the +/// unpinned status summary, a call with no routed alias). It is deliberately +/// NOT the query fallback for a repo that records no model — see +/// `unrecorded_index_is_queried_with_builtin_default_not_serve_default`. +#[test] +fn serve_default_model_is_service_fallback() { + use crate::embed::ModelType; + let state = std::sync::Arc::new( + ServeState::new(ReposConfig::default(), None) + .with_default_model(Some(ModelType::EmbeddingGemma300MQ4)), + ); + assert_eq!(state.default_model(), Some(ModelType::EmbeddingGemma300MQ4)); + + let svc = crate::mcp::CodesearchService::new_for_serve(state).unwrap(); + assert_eq!( + svc.query_model(None), + ModelType::EmbeddingGemma300MQ4, + "serve must fall back to its default model, not the built-in default" + ); +} + +/// Without `--model`, `ServeState` reports no default and the service falls back +/// to the built-in default. +#[test] +fn no_serve_default_keeps_builtin_fallback() { + use crate::embed::ModelType; + let state = std::sync::Arc::new(ServeState::new(ReposConfig::default(), None)); + assert_eq!(state.default_model(), None); + + let svc = crate::mcp::CodesearchService::new_for_serve(state).unwrap(); + assert_eq!(svc.query_model(None), ModelType::default()); +} + +/// A repo whose `metadata.json` records no model is queried with the BUILT-IN +/// default, never the serve-wide `--model` default. +/// +/// Regression guard for `serve --model X` silently overriding a legacy index: +/// with a 768-dim serve default, a 384-dim legacy index failed every search with +/// "Query embedding dimension mismatch: expected 384, got 768", and a +/// same-dimension default would have compared incomparable vector spaces without +/// erroring. The assumption must also reach the caller as a warning naming the +/// repo, the assumed model and the re-index command. +#[test] +fn unrecorded_index_is_queried_with_builtin_default_not_serve_default() { + use crate::embed::ModelType; + + let tmp = tempfile::tempdir().unwrap(); + let config_file = tmp.path().join("repos.json"); + let repo_path = tmp.path().join("legacy"); + std::fs::create_dir(&repo_path).unwrap(); + let mut config = ReposConfig::default(); + config + .register_with_alias(repo_path.clone(), Some("legacy".to_string())) + .unwrap(); + config.save_to(&config_file).unwrap(); + + let state = std::sync::Arc::new( + ServeState::new(config, Some(config_file)) + .with_default_model(Some(ModelType::EmbeddingGemma300MQ4)), + ); + let svc = crate::mcp::CodesearchService::new_for_serve(state).unwrap(); + + // No metadata.json yet: an unrecorded model. Serve default is gemma. + let resolution = svc.resolve_query_model(Some("legacy")); + assert_eq!( + resolution.model, + ModelType::default(), + "a repo that records no model must be queried with the built-in default, \ + not the '{}' serve default", + ModelType::EmbeddingGemma300MQ4.short_name() + ); + let warning = resolution + .assumed_warning + .expect("the assumed model must be surfaced to the caller"); + assert!( + warning.contains("legacy"), + "warning must name the repo: {warning}" + ); + assert!( + warning.contains(ModelType::default().short_name()), + "warning must name the assumed model: {warning}" + ); + assert!( + warning.contains("--force"), + "warning must give the re-index command: {warning}" + ); + + // A recorded model is used as-is and must not warn. + let db_path = repo_path.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + r#"{"model_short_name":"embeddinggemma-q4","dimensions":768}"#, + ) + .unwrap(); + let resolution = svc.resolve_query_model(Some("legacy")); + assert_eq!(resolution.model, ModelType::EmbeddingGemma300MQ4); + assert!( + resolution.assumed_warning.is_none(), + "a recorded model must not warn" + ); +} + +/// The unrecorded-model log warning fires once per alias, so a busy hub does not +/// repeat the same line on every query. The caller-facing warning is separate +/// and is not deduped. +#[test] +fn legacy_model_warning_is_logged_once_per_alias() { + let state = ServeState::new(ReposConfig::default(), None); + assert!(state.mark_legacy_model_warned("a")); + assert!(!state.mark_legacy_model_warned("a")); + assert!(state.mark_legacy_model_warned("b")); +} + /// `persist_config` must write to the override path (and therefore be /// observable by `reload_if_changed`/`config_snapshot`) rather than the real /// `~/.codesearch/repos.json`. Guards the wiring that makes the register @@ -982,10 +1234,15 @@ fn config_reload_no_spurious_reload() { assert_eq!(after_second, after_first); } -/// Verify that the /repos/:alias/reindex route is registered and reachable. +/// Verify that the /repos/{alias}/reindex route is registered and reachable. /// This test starts a real axum server on a random port and sends a POST request. +/// +/// `#[serial]` + env reset — same allowed-roots race guard as the add_repo +/// handler test above. +#[serial] #[tokio::test] async fn reindex_route_is_registered() { + let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let tmp = tempfile::tempdir().unwrap(); let repo_path = tmp.path().join("myrepo"); std::fs::create_dir(&repo_path).unwrap(); @@ -1006,7 +1263,7 @@ async fn reindex_route_is_registered() { axum::routing::get(health_handler), ) .route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post(reindex_handler), ) .with_state(state); @@ -1073,8 +1330,10 @@ async fn reindex_route_is_registered() { /// corpus index it only holds read-only. The handler returns 409 CONFLICT with /// `status: "read_only"` (see the read-only guard in `reindex_handler`, /// src/serve/mod.rs). +#[serial] #[tokio::test] async fn reindex_refused_for_read_only_repo_even_with_force() { + let _env = crate::testing::EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let (_tmp, _repo_path, state) = state_with_repo("readonlyrepo"); // Mark the repo read-only in the live config (how a snapshot-restore sets it). state @@ -1091,7 +1350,7 @@ async fn reindex_refused_for_read_only_repo_even_with_force() { axum::routing::get(health_handler), ) .route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post(reindex_handler), ) .with_state(state); @@ -1181,7 +1440,7 @@ async fn healthz_is_unauthenticated_on_network_bind() { ); } -/// Verify that the /repos/:alias/info and /repos/:alias/doctor routes are +/// Verify that the /repos/{alias}/info and /repos/{alias}/doctor routes are /// registered and reachable. Starts a real axum server on a random port and /// asserts that an unknown alias yields our handler's 404 (not axum's 404). #[tokio::test] @@ -1205,8 +1464,8 @@ async fn info_doctor_routes_registered() { crate::constants::HEALTH_PATH, axum::routing::get(health_handler), ) - .route("/repos/:alias/info", axum::routing::get(info_handler)) - .route("/repos/:alias/doctor", axum::routing::post(doctor_handler)) + .route("/repos/{alias}/info", axum::routing::get(info_handler)) + .route("/repos/{alias}/doctor", axum::routing::post(doctor_handler)) .with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -1294,7 +1553,7 @@ async fn info_doctor_routes_registered() { } /// Verify that the federation REST endpoints (/search, /find, /explore, -/// /chunk/:id) are registered and reachable. Each must dispatch to OUR +/// /chunk/{id}) are registered and reachable. Each must dispatch to OUR /// handler (returning a JSON body) rather than axum's built-in empty 404. /// Starts a real axum server on a random port. #[tokio::test] @@ -1497,7 +1756,7 @@ async fn concurrent_reindex_returns_conflict() { let app = axum::Router::new() .route( - "/repos/:alias/reindex", + "/repos/{alias}/reindex", axum::routing::post(reindex_handler), ) .with_state(state); @@ -1547,20 +1806,17 @@ async fn concurrent_reindex_returns_conflict() { /// Unit tests for `validate_path_within_allowed_roots`. /// -/// These tests temporarily set/remove the `CODESEARCH_ALLOWED_ROOTS` env var. -/// A static Mutex serializes env mutation to prevent races under parallel test execution. +/// These tests mutate the `CODESEARCH_ALLOWED_ROOTS` env var. Per the +/// AGENTS.md rule they are `#[serial]` and restore the var via `EnvRestore`: +/// a private Mutex cannot protect against non-serial tests elsewhere in the +/// process that READ the var through the real handlers (the add_repo +/// handler test below), which is exactly the 403 flake this closed. #[cfg(test)] mod allowed_roots_tests { use super::*; + use crate::testing::EnvRestore; + use serial_test::serial; use std::path::PathBuf; - use std::sync::Mutex; - - /// Global lock to serialize env var mutations across parallel test threads. - static ENV_LOCK: std::sync::OnceLock> = std::sync::OnceLock::new(); - - fn lock() -> std::sync::MutexGuard<'static, ()> { - ENV_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() - } /// Helper: create a unique temp dir per test, return its canonical path. fn temp_root(suffix: &str) -> PathBuf { @@ -1569,57 +1825,49 @@ mod allowed_roots_tests { safe_canonicalize(&dir).unwrap() } - fn clear_env() { - std::env::remove_var(ALLOWED_ROOTS_ENV); - } - - fn set_env(val: &str) { - std::env::set_var(ALLOWED_ROOTS_ENV, val); - } - + #[serial] #[test] fn env_unset_allows_all() { - let _guard = lock(); - clear_env(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let path = PathBuf::from("/some/random/path"); assert!(validate_path_within_allowed_roots(&path).is_ok()); } + #[serial] #[test] fn env_empty_allows_all() { - let _guard = lock(); - set_env(""); + let _env = EnvRestore::set(&[(ALLOWED_ROOTS_ENV, "")]); let path = PathBuf::from("/some/random/path"); assert!(validate_path_within_allowed_roots(&path).is_ok()); - clear_env(); } + #[serial] #[test] fn path_within_root_is_allowed() { - let _guard = lock(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let root = temp_root("within"); - set_env(&root.display().to_string()); + std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string()); let child = root.join("my-project"); let _ = std::fs::create_dir_all(&child); let canonical_child = safe_canonicalize(&child).unwrap(); assert!(validate_path_within_allowed_roots(&canonical_child).is_ok()); - clear_env(); } + #[serial] #[test] fn exact_root_match_is_allowed() { - let _guard = lock(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let root = temp_root("exact"); - set_env(&root.display().to_string()); + std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string()); assert!(validate_path_within_allowed_roots(&root).is_ok()); - clear_env(); } + #[serial] #[test] fn path_outside_root_is_rejected() { - let _guard = lock(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let root = temp_root("outside"); - set_env(&root.display().to_string()); + std::env::set_var(ALLOWED_ROOTS_ENV, root.display().to_string()); // Construct a path guaranteed outside the temp root let outside = if cfg!(windows) { PathBuf::from("C:\\Windows\\System32") @@ -1635,40 +1883,45 @@ mod allowed_roots_tests { let result = validate_path_within_allowed_roots(&outside); assert!(result.is_err(), "Expected rejection for path outside root"); assert!(result.unwrap_err().contains("outside allowed roots")); - clear_env(); } + #[serial] #[test] fn all_nonexistent_roots_rejects() { - let _guard = lock(); - set_env("/nonexistent/path/abc;/also/nonexistent/xyz"); + let _env = EnvRestore::set(&[( + ALLOWED_ROOTS_ENV, + "/nonexistent/path/abc;/also/nonexistent/xyz", + )]); let some_path = std::env::temp_dir(); let canonical = safe_canonicalize(&some_path).unwrap(); let result = validate_path_within_allowed_roots(&canonical); assert!(result.is_err()); assert!(result.unwrap_err().contains("No valid roots found")); - clear_env(); } + #[serial] #[test] fn semicolons_with_empty_segments_works() { - let _guard = lock(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let root = temp_root("semicolons"); - set_env(&format!(";{};;", root.display())); + std::env::set_var(ALLOWED_ROOTS_ENV, format!(";{};;", root.display())); let child = root.join("project"); let _ = std::fs::create_dir_all(&child); let canonical_child = safe_canonicalize(&child).unwrap(); assert!(validate_path_within_allowed_roots(&canonical_child).is_ok()); - clear_env(); } + #[serial] #[test] fn multiple_roots_any_match() { - let _guard = lock(); + let _env = EnvRestore::remove(&[ALLOWED_ROOTS_ENV]); let root1 = temp_root("multi1"); let root2 = temp_root("multi2"); - set_env(&format!("{};{}", root1.display(), root2.display())); + std::env::set_var( + ALLOWED_ROOTS_ENV, + format!("{};{}", root1.display(), root2.display()), + ); // Path under root1 let child1 = root1.join("project"); @@ -1681,8 +1934,6 @@ mod allowed_roots_tests { let _ = std::fs::create_dir_all(&child2); let canonical2 = safe_canonicalize(&child2).unwrap(); assert!(validate_path_within_allowed_roots(&canonical2).is_ok()); - - clear_env(); } } @@ -2130,7 +2381,7 @@ async fn indexing_route_answers_json() { /// Shared envelope for the Layer-2 e2e tests below: pin the delegation env /// vars to a fresh temp repos.json, seed it via `seed`, spawn a REAL serve /// (the two routes the CLI `index rm` delegation touches: `GET /health` and -/// the real `remove_repo_handler` at `DELETE /repos/:alias`) sharing that +/// the real `remove_repo_handler` at `DELETE /repos/{alias}`) sharing that /// config, and wait (bounded, panicking on timeout) for it to accept. /// /// Every step here is trap-sensitive, which is why it is a helper and not @@ -2181,7 +2432,7 @@ where crate::constants::HEALTH_PATH, axum::routing::get(health_handler), ) - .route("/repos/:alias", axum::routing::delete(remove_repo_handler)) + .route("/repos/{alias}", axum::routing::delete(remove_repo_handler)) .with_state(state.clone()); tokio::spawn(async move { let _ = axum::serve(listener, app).await; @@ -2205,7 +2456,7 @@ where /// The full Layer-2 acceptance path: a running serve instance holds the /// repo's registration; `remove_from_index` (the CLI code path) must -/// DELEGATE to it (health probe → DELETE /repos/:alias), serve must stop +/// DELEGATE to it (health probe → DELETE /repos/{alias}), serve must stop /// holders and delete the DB directory WITHOUT being stopped, repos.json /// must lose the entry, and a later query for the alias must be a clean /// "Unknown alias" (no zombie stores) — all without stopping serve. @@ -2299,7 +2550,7 @@ async fn index_rm_deletes_db_while_serve_holds_real_lmdb_env() { // Serve opens the repo FOR REAL — a live LMDB env under db_path. let opened = state - .try_open_stores("heldenv", &db_path, true, false) + .try_open_stores("heldenv", &db_path, true, false, None) .expect("opening a real store for a brand-new repo must succeed"); let OpenedStores::Write(stores) = opened else { panic!("brand-new repo must open Write, not Readonly"); @@ -2397,3 +2648,237 @@ fn evicting_idle_repo_clears_frozen_csharp_error_state() { "eviction must clear the cached C# error message along with the status" ); } + +/// The model a serve query is embedded with is read from the routed repo's own +/// index metadata — never assumed to be the hub-wide default. +/// +/// Regression guard for the serve hub pinning `ModelType::default()` (384-dim +/// MiniLM) for every query: on a hub whose indexes were rebuilt with +/// EmbeddingGemma that failed with "Query embedding dimension mismatch: +/// expected 768, got 384". Reintroducing the default pin makes the gemma cases +/// below fail. +#[test] +fn model_for_alias_reads_the_index_metadata_model() { + let cases = [ + ( + "embeddinggemma-q4", + Some(crate::embed::ModelType::EmbeddingGemma300MQ4), + ), + ("minilm-l6-q", Some(crate::embed::ModelType::AllMiniLML6V2Q)), + ("bge-base", Some(crate::embed::ModelType::BGEBaseENV15)), + // An unknown recorded name must not be silently coerced to the default: + // callers fall back explicitly, and the resolver reports "no answer". + ("not-a-real-model", None), + ]; + + for (model_short_name, expected) in cases { + let (_tmp, repo_path, state) = state_with_repo("repo"); + let db_path = repo_path.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + format!(r#"{{"model_short_name":"{model_short_name}","dimensions":768}}"#), + ) + .unwrap(); + + assert_eq!( + state.model_for_alias("repo"), + expected, + "metadata model_short_name '{model_short_name}' must drive the query model" + ); + } +} + +/// Missing metadata (unindexed / legacy index) yields `None`, so the caller's +/// documented fallback to the default applies — and an unknown alias cannot +/// borrow another repo's model. +#[test] +fn model_for_alias_is_none_without_index_metadata() { + let (_tmp, _repo_path, state) = state_with_repo("repo"); + assert_eq!(state.model_for_alias("repo"), None); + assert_eq!(state.model_for_alias("not-registered"), None); +} + +/// A single hub can hold indexes built with different models: each alias +/// resolves independently, so a group fan-out embeds each store's query with +/// that store's own model. +#[test] +fn model_for_alias_is_per_repo_not_hub_wide() { + let tmp = tempfile::tempdir().unwrap(); + let config_file = tmp.path().join("repos.json"); + let mut config = ReposConfig::default(); + for (alias, model) in [("legacy", "minilm-l6-q"), ("rebuilt", "embeddinggemma-q4")] { + let repo_path = tmp.path().join(alias); + std::fs::create_dir(&repo_path).unwrap(); + config + .register_with_alias(repo_path.clone(), Some(alias.to_string())) + .unwrap(); + let db_path = repo_path.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + format!(r#"{{"model_short_name":"{model}"}}"#), + ) + .unwrap(); + } + config.save_to(&config_file).unwrap(); + let state = ServeState::new(config, Some(config_file)); + + assert_eq!( + state.model_for_alias("legacy"), + Some(crate::embed::ModelType::AllMiniLML6V2Q) + ); + assert_eq!( + state.model_for_alias("rebuilt"), + Some(crate::embed::ModelType::EmbeddingGemma300MQ4) + ); +} + +/// The serve MCP service resolves the query model through the routed repo, not +/// its own (default) field. This is the exact seam the hub got wrong: it is the +/// service, not `ServeState`, that hands the model to the embedder. +#[test] +fn serve_service_uses_repo_model_not_default() { + let (_tmp, repo_path, state) = state_with_repo("gemma-repo"); + let db_path = repo_path.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + r#"{"model_short_name":"embeddinggemma-q4","dimensions":768}"#, + ) + .unwrap(); + + let svc = crate::mcp::CodesearchService::new_for_serve(std::sync::Arc::new(state)).unwrap(); + + assert_eq!( + svc.query_model(Some("gemma-repo")), + crate::embed::ModelType::EmbeddingGemma300MQ4, + "serve must embed a repo's queries with the model that repo was indexed with" + ); + // No alias (unscoped) or an unknown alias falls back to the service default. + assert_eq!(svc.query_model(None), crate::embed::ModelType::default()); + assert_eq!( + svc.query_model(Some("not-registered")), + crate::embed::ModelType::default() + ); +} + +/// The grouped `status` model label must reflect the members' recorded models, +/// not the service default: a same-model group names that model, a mixed-model +/// hub says `mixed`. Regression guard for the status field reporting the +/// hardcoded default (`minilm-l6-q`) for every repo. +#[test] +fn group_status_model_label_is_common_or_mixed() { + let tmp = tempfile::tempdir().unwrap(); + let config_file = tmp.path().join("repos.json"); + let mut config = ReposConfig::default(); + for (alias, model) in [("legacy", "minilm-l6-q"), ("rebuilt", "embeddinggemma-q4")] { + let repo_path = tmp.path().join(alias); + std::fs::create_dir(&repo_path).unwrap(); + config + .register_with_alias(repo_path.clone(), Some(alias.to_string())) + .unwrap(); + let db_path = repo_path.join(DB_DIR_NAME); + std::fs::create_dir_all(&db_path).unwrap(); + std::fs::write( + db_path.join("metadata.json"), + format!(r#"{{"model_short_name":"{model}"}}"#), + ) + .unwrap(); + } + config.save_to(&config_file).unwrap(); + let state = std::sync::Arc::new(ServeState::new(config, Some(config_file))); + let svc = crate::mcp::CodesearchService::new_for_serve(state).unwrap(); + + assert_eq!( + svc.group_model_label(&["legacy".to_string(), "rebuilt".to_string()]), + "mixed", + "a hub holding indexes built with different models must report 'mixed'" + ); + assert_eq!( + svc.group_model_label(&["rebuilt".to_string()]), + "embeddinggemma-q4", + "a single-model group must name its base model" + ); +} + +/// A fresh repo added with a model override must open its store at that model's +/// dimension, not the 384-dim default. Regression guard for `POST /repos` with +/// `model=embeddinggemma-q4`: the store used to be created at 384 and the +/// override applied only to metadata, so the reindex embedded 768-dim vectors +/// into a 384-dim store and indexed nothing. +#[tokio::test] +async fn try_open_stores_honours_dimension_override_for_a_fresh_repo() { + let tmp = tempfile::tempdir().unwrap(); + let repo_path = tmp.path().join("gemmarepo"); + std::fs::create_dir(&repo_path).unwrap(); + let db_path = repo_path.join(DB_DIR_NAME); + assert!(!db_path.exists(), "precondition: db dir must not exist yet"); + + let state = state_with_config(ReposConfig::default()); + + let stores = match state.try_open_stores("gemmarepo", &db_path, true, false, Some(768)) { + Ok(OpenedStores::Write(s)) => s, + Ok(OpenedStores::Readonly(_)) => panic!("expected Write, got Readonly"), + Err(e) => panic!("fresh open with a dimension override must succeed, got: {e}"), + }; + + let dims = stores + .vector_store + .read() + .await + .stats() + .expect("stats on a freshly created store") + .dimensions; + assert_eq!( + dims, 768, + "a repo added with --model embeddinggemma-q4 must open at 768 dims, not the 384 default" + ); +} + +#[test] +fn is_lmdb_format_corruption_matches_known_lmdb_errors() { + let cases = [ + ( + "MDB_BAD_VALSIZE: Unsupported size of key/DB name/data, or wrong DUPFIXED size", + true, + ), + ("Symbol rebuild failed: heed -> MDB_BAD_VALSIZE", true), + ("storage error: wrong DUPFIXED size", true), + ("Unsupported size of key while opening vectordb", true), + ("scip-csharp failed with exit code 1", false), + ("MDB_NOTFOUND: No matching key/data pair found", false), + ("Task panicked: workspace load failed", false), + ("", false), + ]; + for (msg, expected) in cases { + assert_eq!( + ServeState::is_lmdb_format_corruption(msg), + expected, + "unexpected classification for {msg:?}" + ); + } +} + +#[tokio::test] +async fn enqueue_format_recovery_dedupes_and_starts_single_worker() { + let state = Arc::new(ServeState::new(ReposConfig::default(), None)); + // Same alias three times must collapse to one queued entry. The recovery + // worker is spawned but (current-thread test runtime) does not execute + // until an await point, so the queue content is asserted deterministically. + state.enqueue_format_recovery("ghost-repo"); + state.enqueue_format_recovery("ghost-repo"); + state.enqueue_format_recovery("ghost-repo"); + let len = state + .format_recovery_queue + .lock() + .expect("queue lock") + .len(); + assert_eq!(len, 1, "duplicate enqueues must collapse to one entry"); + assert!( + state + .format_recovery_worker_started + .load(std::sync::atomic::Ordering::Acquire), + "the first enqueue must start the recovery worker" + ); +} diff --git a/src/serve/tui.rs b/src/serve/tui.rs index de7ea5d8..9d31e60e 100644 --- a/src/serve/tui.rs +++ b/src/serve/tui.rs @@ -1060,7 +1060,7 @@ fn with_remote_stats(overlay: OverlayState, stats: RemoteStatsState) -> OverlayS /// Outcome of a TUI force-reindex launch — used to drive immediate footer /// feedback. Only describes whether the background task *started*; the actual /// indexing result is reported later via the status column / logs. -enum ReindexLaunch { +pub(crate) enum ReindexLaunch { /// Background reindex task spawned successfully. Started, /// A reindex was already running for this alias — request ignored. @@ -1071,7 +1071,7 @@ enum ReindexLaunch { /// Spawn a background force reindex task for the given repo alias. /// Follows the same flow as the HTTP `reindex_handler`. -fn spawn_force_reindex(alias: String, state: &Arc) -> ReindexLaunch { +pub(crate) fn spawn_force_reindex(alias: String, state: &Arc) -> ReindexLaunch { // Guard against concurrent reindex if !state.begin_indexing(&alias) { tracing::warn!( @@ -1120,7 +1120,7 @@ fn spawn_force_reindex(alias: String, state: &Arc) -> ReindexLaunch None => { // Try to open stores (allow_create=true for recovery) let cancel = CancellationToken::new(); - match state.try_open_stores(&alias, &db_path, true, false) { + match state.try_open_stores(&alias, &db_path, true, false, None) { Ok(super::OpenedStores::Write(s)) => { state.repos.insert( alias.clone(), diff --git a/src/watch/mod.rs b/src/watch/mod.rs index 92abbcfa..42ddf268 100644 --- a/src/watch/mod.rs +++ b/src/watch/mod.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Result}; use ignore::gitignore::{Gitignore, GitignoreBuilder}; -use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap}; +use notify::{RecommendedWatcher, RecursiveMode}; +use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache}; use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::process::Command; @@ -67,7 +67,7 @@ pub enum FileEvent { /// 3. Batched events for efficient processing pub struct FileWatcher { root: PathBuf, - debouncer: Option>, + debouncer: Option>, receiver: Option>, /// Compiled .gitignore matcher for the repo root (None if no .gitignore found). gitignore: Option, @@ -195,17 +195,12 @@ impl FileWatcher { self.receiver = Some(rx); self.debouncer = Some(debouncer); - // Start watching the root directory + // Start watching the root directory (Debouncer implements Watcher directly + // since notify-debouncer-full 0.7 and tracks file-ID cache roots itself) if let Some(ref mut debouncer) = self.debouncer { debouncer - .watcher() .watch(&self.root, RecursiveMode::Recursive) .map_err(|e| anyhow!("Failed to watch directory: {}", e))?; - - // Also watch with the cache (for file ID tracking) - debouncer - .cache() - .add_root(&self.root, RecursiveMode::Recursive); } Ok(()) @@ -219,7 +214,7 @@ impl FileWatcher { /// Stop watching pub fn stop(&mut self) { if let Some(ref mut debouncer) = self.debouncer { - let _ = debouncer.watcher().unwatch(&self.root); + let _ = debouncer.unwatch(&self.root); } self.debouncer = None; self.receiver = None;