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
Found by bug-hunt wave 29 (crate-as-library lens — the
cargo add turbovecexperience). All compiled against the crate as a path dependency.1. HIGH —
SearchResultsimplements no traits at all.turbovec/src/lib.rs:185has no#[derive(...)], while every other public type hasDebug(TurboQuantIndex lib.rs:127, IdMapIndex id_map.rs:83) and all three error enums haveDebug + Clone + PartialEqplus Error/Display.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:
TurboQuantIndexandIdMapIndexareSend + Sync + Debug,Arc<RwLock<TurboQuantIndex>>compiles, and all three error enums satisfyError + Send + Sync + 'static(anyhow/thiserror work) and are#[non_exhaustive].2. HIGH —
searchhas no non-panicking form, though the Python bindings have one.lib.rs:608/626panic on a query buffer that isn't a multiple ofdim(assert_eq! at :645) and on any non-finite or huge coordinate (explicit panic! at :653);id_map.rs:347panics 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-rollfirst_invalid_coord+ length +containschecks per call, duplicating logic that already exists in the binding. Gap: notry_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:Newest std API used is
div_ceil/next_multiple_of(1.73) andOnceLock(1.70); the real floor is rayon's 1.80. pyo3 0.29 and numpy 0.29 both declarerust-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 toadd(should beSelf::add); search.rs:92 unresolvedblock_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 vswrite!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 forBeta::cdf/pdf; encode.rs:461,463 forinverse_cdf), all symmetricBeta(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::searchreturns a bare tuple whileTurboQuantIndex::searchreturns a typed struct.id_map.rs:319returns(Vec<f32>, Vec<u64>)with no nq/k, so the caller must reconstruct the stride asscores.len() / nq— the exact footgunSearchResults.kexists to prevent on the other index. Probe:m.search(queries, 10)on a 3-vector index returnedids.len() == 6for 2 queries, i.e. stride 3, not 10; a user writing the obvious&ids[q*10..]gets a panic or wrong rows.IdMapIndexalso exposes no iterator over its ids/slots.7. MEDIUM —
examples/downstream-smoke/covers roughly a third of what a real embedder does. It exercisesnew → add → prepare → search → write → loadon TurboQuantIndex only. Never exercised, and so never protected on the downstream build path:IdMapIndexentirely (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 andArc<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; andk > lenclamping (it uses K=5, N_DB=256, so it never seesresults.k != K). Adding trait assertions (fn assert<T: Send + Sync + Debug>()) to this file would have caught finding 1 mechanically.Informational:
lib.rs:44'scompile_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