Skip to content

Rust crate as a downstream dependency: SearchResults derives nothing, search() has no non-panicking form, MSRV over-declared by 9 releases #351

Description

@RyanCodrai

Found by bug-hunt wave 29 (crate-as-library lens — the cargo add turbovec experience). All compiled against the crate as a path dependency.

1. HIGH — SearchResults implements no traits at all. turbovec/src/lib.rs:185 has no #[derive(...)], while every other public type has Debug (TurboQuantIndex lib.rs:127, IdMapIndex id_map.rs:83) and all three error enums have Debug + Clone + PartialEq plus Error/Display.

error[E0277]: `SearchResults` doesn't implement `Debug`
error[E0277]: the trait bound `SearchResults: Clone` is not satisfied

A downstream struct holding one can't #[derive(Debug)], dbg!(results) fails, assert_eq! in a user's test fails, and it can't be cached or cloned — on the type every search returns. #[derive(Debug, Clone, PartialEq)] is a one-line fix.

Verified good by the same probe: TurboQuantIndex and IdMapIndex are Send + Sync + Debug, Arc<RwLock<TurboQuantIndex>> compiles, and all three error enums satisfy Error + Send + Sync + 'static (anyhow/thiserror work) and are #[non_exhaustive].

2. HIGH — search has no non-panicking form, though the Python bindings have one. lib.rs:608/626 panic on a query buffer that isn't a multiple of dim (assert_eq! at :645) and on any non-finite or huge coordinate (explicit panic! at :653); id_map.rs:347 panics on an allowlist id not in the index. Meanwhile turbovec-python/src/lib.rs:781-802 pre-validates the same three conditions and raises ValueError/KeyError. So Python callers get recoverable errors and Rust callers get an aborted request thread. The add path got the typed-error treatment (add_2d -> Result<_, AddError>); the search path didn't. A service taking query vectors or filter id-lists from users must hand-roll first_invalid_coord + length + contains checks per call, duplicating logic that already exists in the binding. Gap: no try_search/search_checked -> Result<SearchResults, SearchError>. (Related to #318, which covers the allowlist panics specifically.)

3. MEDIUM — declared MSRV 1.89 is ~9 releases higher than the crate needs, locking out users on 1.85–1.88 for no reason. turbovec/Cargo.toml:5. Verified by building:

cargo +1.83 build -p turbovec --ignore-rust-version  -> Finished
cargo +1.81 build -p turbovec --ignore-rust-version  -> Finished
cargo +1.83 check -p turbovec --all-targets          -> Finished (warnings only)

Newest std API used is div_ceil/next_multiple_of (1.73) and OnceLock (1.70); the real floor is rayon's 1.80. pyo3 0.29 and numpy 0.29 both declare rust-version = "1.83", so the python crate doesn't force it either. Cargo hard-fails (rustc 1.83.0 is not supported by … requires rustc 1.89), and the CI leg (ci.yml:60) only proves the declared value builds — it can never catch an over-declaration.

4. MEDIUM — docs.rs will render with 13 rustdoc warnings including 2 dead links, and RUSTDOCFLAGS="-D warnings" makes the doc build fail outright: lib.rs:746 unresolved link to add (should be Self::add); search.rs:92 unresolved block_pair_has_allowed; search.rs:91 and rotation.rs:100 link public docs to private items (render as plain text); io.rs ×6 ambiguous [`write`] (function vs write! macro — needs [`write()`]); id_map.rs:188-190 3× redundant explicit link target.

5. MEDIUM — 79% of the dependency tree exists to evaluate a Beta distribution. statrs = "0.17" is used at exactly three sites (codebook.rs:24,53,65 for Beta::cdf/pdf; encode.rs:461,463 for inverse_cdf), all symmetric Beta(a, a). cargo tree -p turbovec --edges normal = 38 crates, 30 of them under statrs — all of nalgebra 0.32, matrixmultiply, simba, num-complex/rational/bigint, rand_distr, and a syn/quote/proc-macro2 chain (a serialized build-graph choke point). That's a linear-algebra stack compiled by every downstream user so the codebook can call an incomplete-beta function, and it undercuts the "no native dependency" pitch — the ceremony moved from linker flags to ~30 s of cold-build time. A ~60-line regularized-incomplete-beta plus bisection inverse would drop the tree from 38 to 8. There's also no [features] section at all, so nobody can opt out. (See #346: this same dependency is an unpinned format-determinism hazard.)

6. MEDIUM — IdMapIndex::search returns a bare tuple while TurboQuantIndex::search returns a typed struct. id_map.rs:319 returns (Vec<f32>, Vec<u64>) with no nq/k, so the caller must reconstruct the stride as scores.len() / nq — the exact footgun SearchResults.k exists to prevent on the other index. Probe: m.search(queries, 10) on a 3-vector index returned ids.len() == 6 for 2 queries, i.e. stride 3, not 10; a user writing the obvious &ids[q*10..] gets a panic or wrong rows. IdMapIndex also exposes no iterator over its ids/slots.

7. MEDIUM — examples/downstream-smoke/ covers roughly a third of what a real embedder does. It exercises new → add → prepare → search → write → load on TurboQuantIndex only. Never exercised, and so never protected on the downstream build path: IdMapIndex entirely (the API most apps want); any error path (add_2d's Result, matching AddError, #[non_exhaustive] wildcard arms); any mutation (swap_remove, remove) or add-after-search cache invalidation; Send/Sync and Arc<RwLock<…>> — the crate's headline concurrency claim at lib.rs:22-35 is untested from downstream; to_bytes/from_bytes, write_with_durability + io::Durability, search_with_mask, from_parts; and k > len clamping (it uses K=5, N_DB=256, so it never sees results.k != K). Adding trait assertions (fn assert<T: Send + Sync + Debug>()) to this file would have caught finding 1 mechanically.

Informational: lib.rs:44's compile_error!("turbovec requires a 64-bit target") rules out wasm32 entirely, with a documented rationale (unchecked usize arithmetic). Not gratuitous, but a downstream app with a wasm target gets a hard failure in a dep it can't patch.

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-human-decisionBlocked on a maintainer decision: scope, public claims, or dependency/release policy

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions