diff --git a/Cargo.toml b/Cargo.toml index 0d332f5..6542cca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,94 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/altair823/kebab" version = "0.17.2" +# pre-v0.18 workspace-wide cleanup: enable clippy::pedantic group with +# intentional allow-list. The allowed lints are either cosmetic (doc style), +# informational (function size), or carry intentional truncation we accept +# (numeric casts in tokenizer/ONNX inputs, hash modular reduction, etc). +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +# Intentional u32 ↔ i64 casts in kebab-nli (ONNX i64 inputs from tokenizer u32 ids). +# u64 ↔ usize across kebab-store-sqlite row counts. Wide truncation is auditable +# at use site, not lint-wide. +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_sign_loss = "allow" +cast_precision_loss = "allow" +# Doc markdown style is cosmetic; we run rustdoc on demand. +doc_markdown = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +# Informational only — splitting a long pipeline function isn't always cleaner. +too_many_lines = "allow" +# `Foo::default()` is concise and idiomatic here; `::default()` +# adds noise without surfacing intent. +default_trait_access = "allow" +# Module name prefix on public items keeps the wire/log surface readable +# (`refusal_reason::no_chunks` etc). +module_name_repetitions = "allow" +# We use `#[must_use]` deliberately on public results, not blanket. +must_use_candidate = "allow" +# `String` arg sometimes signals "I'll consume this" — let signature decide. +needless_pass_by_value = "allow" +# Idiomatic single-line bindings stay; let-else expansion isn't always clearer. +manual_let_else = "allow" +# `use` after `let` is a common kebab pattern (scoped imports next to use site). +items_after_statements = "allow" +# Naming pairs like `chunk_id` / `chunks_id` are intentional domain terms. +similar_names = "allow" +# `iter.map(format!).collect::()` is idiomatic when the per-element +# string is genuinely independent — `fold` only wins on accumulation patterns. +format_collect = "allow" +# Exhaustive `match` with explicit variant arms (vs `_`) catches future +# variant additions at compile time (kebab core's `RefusalReason` pattern). +match_wildcard_for_single_variants = "allow" +# Copy types under `&self` keep call-site discipline; auto-deref noise > tiny perf gain. +trivially_copy_pass_by_ref = "allow" +# `unnecessary_wraps` flags helpers that could drop `Result`, but keeping the +# Result allows future error variants without churning callers. +unnecessary_wraps = "allow" +# NLI score / RRF fusion / similarity threshold comparisons are intentional — +# floats live in the `[0, 1]` band and are compared with explicit thresholds. +float_cmp = "allow" +# File-extension dispatch is keyed on ASCII conventions; case sensitivity +# is part of the spec for `.md`, `.pdf`, etc. +case_sensitive_file_extension_comparisons = "allow" +# Config / opts structs intentionally bundle boolean flags (ingest options, +# search modes, etc) — splitting them into enums would obscure the wire shape. +struct_excessive_bools = "allow" +# `bytecount` crate would be a new dep just for one-off ASCII counts. +naive_bytecount = "allow" +# `#[ignore]` annotations on tests document via the test name + nearby comment. +ignore_without_reason = "allow" +# `format!` push patterns are a hot path for kebab-tui's progressive rendering; +# `write!` rewrite needs a verified-equal benchmark before swapping. +format_push_string = "allow" +# Builder-style `with_*` methods return `Self`; the existing `#[must_use]` +# discipline lives on aggregate constructors, not every chainable setter. +return_self_not_must_use = "allow" +# Match arms grouped by side-effect over body equality (e.g. snake_case wire +# label tables) — fanning them out keeps adding a new variant trivial. +match_same_arms = "allow" +# Remaining style-only warnings: trailing `continue` is sometimes clearer than +# rewriting, `_x` underscored bindings document intent at the use site, and +# `!(a == b)` reads better than `a != b` when paired with a complementary check. +needless_continue = "allow" +used_underscore_binding = "allow" +nonminimal_bool = "allow" +# Other one-off cosmetic items: large literal formatting, doc link quoting, +# `Clone::clone_from` swap, `str::replace` chaining, `Iterator::any` ergonomics. +unreadable_literal = "allow" +many_single_char_names = "allow" +doc_link_with_quotes = "allow" +assigning_clones = "allow" +collapsible_str_replace = "allow" +trivial_regex = "allow" +elidable_lifetime_names = "allow" +range_plus_one = "allow" +explicit_iter_loop = "allow" +implicit_hasher = "allow" +ref_option = "allow" + [workspace.dependencies] anyhow = "1" thiserror = "2" diff --git a/crates/kebab-app/Cargo.toml b/crates/kebab-app/Cargo.toml index 164e72c..b506bba 100644 --- a/crates/kebab-app/Cargo.toml +++ b/crates/kebab-app/Cargo.toml @@ -81,3 +81,6 @@ lopdf = "0.32" # error_wire::tests::llm_unreachable_classifies_to_model_unreachable needs a real # reqwest::Error (private constructor) — built from a connect-refused call. reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } + +[lints] +workspace = true diff --git a/crates/kebab-app/src/app.rs b/crates/kebab-app/src/app.rs index 7eabe82..6a3043f 100644 --- a/crates/kebab-app/src/app.rs +++ b/crates/kebab-app/src/app.rs @@ -293,7 +293,7 @@ impl App { // so other in-flight searches can use the cache concurrently. drop(guard); let hits = self.search_uncached(query)?; - let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner()); + let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner); guard.put(key, hits.clone()); Ok(hits) } @@ -467,7 +467,7 @@ impl App { // Snippet truncation if opts.snippet_chars set (mirror non-trace path). if opts.snippet_chars.is_some() { - for h in hits.iter_mut() { + for h in &mut hits { if h.snippet.chars().count() > snippet_chars { h.snippet = trim_to_chars(&h.snippet, snippet_chars); } @@ -502,7 +502,7 @@ impl App { // `config.search.snippet_chars`; this only kicks in when the // caller asked for *less*). if opts.snippet_chars.is_some() { - for h in hits.iter_mut() { + for h in &mut hits { if h.snippet.chars().count() > snippet_chars { h.snippet = trim_to_chars(&h.snippet, snippet_chars); } @@ -521,7 +521,7 @@ impl App { { current_snippet_cap = (current_snippet_cap / 2).max(SNIPPET_FLOOR); - for h in hits.iter_mut() { + for h in &mut hits { if h.snippet.chars().count() > current_snippet_cap { h.snippet = trim_to_chars(&h.snippet, current_snippet_cap); @@ -868,7 +868,7 @@ impl App { /// clear` admin command). No-op when the cache is disabled. pub fn clear_search_cache(&self) { if let Some(cache) = self.search_cache.as_ref() { - let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner()); + let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner); guard.clear(); } } diff --git a/crates/kebab-app/src/bulk.rs b/crates/kebab-app/src/bulk.rs index 6ba14bf..1971682 100644 --- a/crates/kebab-app/src/bulk.rs +++ b/crates/kebab-app/src/bulk.rs @@ -139,9 +139,8 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> { let k = obj .get("k") - .and_then(|v| v.as_u64()) - .map(|n| n as usize) - .unwrap_or(0); // 0 → use config default in app + .and_then(serde_json::Value::as_u64) + .map_or(0, |n| n as usize); // 0 → use config default in app let trust_min = match obj.get("trust_min").and_then(|v| v.as_str()) { None => None, @@ -209,14 +208,14 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> { let opts = SearchOpts { max_tokens: obj .get("max_tokens") - .and_then(|v| v.as_u64()) + .and_then(serde_json::Value::as_u64) .map(|n| n as usize), snippet_chars: obj .get("snippet_chars") - .and_then(|v| v.as_u64()) + .and_then(serde_json::Value::as_u64) .map(|n| n as usize), cursor: obj.get("cursor").and_then(|v| v.as_str()).map(String::from), - trace: obj.get("trace").and_then(|v| v.as_bool()).unwrap_or(false), + trace: obj.get("trace").and_then(serde_json::Value::as_bool).unwrap_or(false), }; Ok(( diff --git a/crates/kebab-app/src/error_wire.rs b/crates/kebab-app/src/error_wire.rs index 9ded9d3..3df41d9 100644 --- a/crates/kebab-app/src/error_wire.rs +++ b/crates/kebab-app/src/error_wire.rs @@ -91,7 +91,7 @@ pub fn classify(err: &anyhow::Error, verbose: bool) -> ErrorV1 { } let mut details = json!({}); if verbose { - let chain: Vec = err.chain().map(|c| c.to_string()).collect(); + let chain: Vec = err.chain().map(std::string::ToString::to_string).collect(); details = json!({"chain": chain}); } ErrorV1 { diff --git a/crates/kebab-app/src/external.rs b/crates/kebab-app/src/external.rs index 7a51990..48d396d 100644 --- a/crates/kebab-app/src/external.rs +++ b/crates/kebab-app/src/external.rs @@ -50,7 +50,7 @@ pub fn ensure_kebabignore_entry(workspace_root: &Path) -> Result<()> { if !existing.is_empty() && !existing.ends_with('\n') { file.write_all(b"\n")?; } - writeln!(file, "{}", KEBABIGNORE_LINE)?; + writeln!(file, "{KEBABIGNORE_LINE}")?; Ok(()) } diff --git a/crates/kebab-app/src/ingest_progress.rs b/crates/kebab-app/src/ingest_progress.rs index b3255f6..597ba81 100644 --- a/crates/kebab-app/src/ingest_progress.rs +++ b/crates/kebab-app/src/ingest_progress.rs @@ -166,8 +166,8 @@ mod tests { }; let v = serde_json::to_value(&ev).unwrap(); assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("asset_started")); - assert_eq!(v.get("idx").and_then(|n| n.as_u64()), Some(1)); - assert_eq!(v.get("total").and_then(|n| n.as_u64()), Some(10)); + assert_eq!(v.get("idx").and_then(serde_json::Value::as_u64), Some(1)); + assert_eq!(v.get("total").and_then(serde_json::Value::as_u64), Some(10)); assert_eq!(v.get("path").and_then(|s| s.as_str()), Some("notes/foo.md")); assert_eq!(v.get("media").and_then(|s| s.as_str()), Some("markdown")); } @@ -184,8 +184,8 @@ mod tests { let v = serde_json::to_value(&ev).unwrap(); assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("completed")); let counts = v.get("counts").unwrap(); - assert_eq!(counts.get("scanned").and_then(|n| n.as_u64()), Some(5)); - assert_eq!(counts.get("new").and_then(|n| n.as_u64()), Some(2)); + assert_eq!(counts.get("scanned").and_then(serde_json::Value::as_u64), Some(5)); + assert_eq!(counts.get("new").and_then(serde_json::Value::as_u64), Some(2)); } #[test] diff --git a/crates/kebab-app/src/lib.rs b/crates/kebab-app/src/lib.rs index e4ad89c..1eb8caf 100644 --- a/crates/kebab-app/src/lib.rs +++ b/crates/kebab-app/src/lib.rs @@ -289,8 +289,7 @@ pub fn ingest_with_config_opts( let cancelled = || { opts.cancel .as_ref() - .map(|c| c.load(std::sync::atomic::Ordering::Relaxed)) - .unwrap_or(false) + .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed)) }; let force_reingest = opts.force_reingest; let started_instant = std::time::Instant::now(); @@ -394,7 +393,7 @@ pub fn ingest_with_config_opts( let purged_deleted_files = sweep_deleted_files( &app, &scanned_paths, - vector_store.as_ref().map(|v| v.as_ref()), + vector_store.as_ref().map(std::convert::AsRef::as_ref), )?; let started_at = time::OffsetDateTime::now_utc(); @@ -509,10 +508,10 @@ pub fn ingest_with_config_opts( *skipped_by_extension.entry(ext).or_insert(0) += 1; } kebab_core::IngestItemKind::Unchanged => { - unchanged_count = unchanged_count.saturating_add(1) + unchanged_count = unchanged_count.saturating_add(1); } kebab_core::IngestItemKind::Error => { - error_count = error_count.saturating_add(1) + error_count = error_count.saturating_add(1); } } crate::ingest_progress::emit( @@ -940,9 +939,7 @@ fn try_skip_unchanged( fn ext_for_skip_warning(path: &str) -> String { std::path::Path::new(path) .extension() - .and_then(|s| s.to_str()) - .map(|s| s.to_ascii_lowercase()) - .unwrap_or_else(|| NO_EXT_SENTINEL.to_string()) + .and_then(|s| s.to_str()).map_or_else(|| NO_EXT_SENTINEL.to_string(), str::to_ascii_lowercase) } /// p9-fb-25: render the `IngestItem.warnings` line for a Skipped @@ -2407,7 +2404,7 @@ fn lang_hint_from_doc(doc: &CanonicalDocument) -> Option { /// Convenience: end byte of the frontmatter region (or 0 when absent). fn fm_span_end(span: Option) -> usize { - span.map(|s| s.end).unwrap_or(0) + span.map_or(0, |s| s.end) } /// Count `\n` in a byte prefix to convert frontmatter byte span to @@ -2710,8 +2707,7 @@ pub fn ingest_file_with_config( const SUPPORTED_EXTS: &[&str] = &["md", "pdf", "png", "jpg", "jpeg"]; if !SUPPORTED_EXTS.contains(&ext.as_str()) { anyhow::bail!( - "ingest-file: unsupported extension `.{}` (supported: {:?})", - ext, SUPPORTED_EXTS + "ingest-file: unsupported extension `.{ext}` (supported: {SUPPORTED_EXTS:?})" ); } diff --git a/crates/kebab-app/src/schema.rs b/crates/kebab-app/src/schema.rs index c8e4fd4..daab650 100644 --- a/crates/kebab-app/src/schema.rs +++ b/crates/kebab-app/src/schema.rs @@ -165,7 +165,7 @@ fn collect_stats( store: &kebab_store_sqlite::SqliteStore, ) -> anyhow::Result { let counts = store - .count_summary_with_threshold(cfg.search.stale_threshold_days as u64)?; + .count_summary_with_threshold(u64::from(cfg.search.stale_threshold_days))?; let data_dir = kebab_config::expand_path(&cfg.storage.data_dir, ""); let index_bytes = kebab_store_sqlite::stats_ext::index_bytes(&data_dir) .map_err(|e| anyhow::anyhow!("index_bytes: {e}"))?; diff --git a/crates/kebab-app/tests/code_ingest_smoke.rs b/crates/kebab-app/tests/code_ingest_smoke.rs index 28d1d05..6c01119 100644 --- a/crates/kebab-app/tests/code_ingest_smoke.rs +++ b/crates/kebab-app/tests/code_ingest_smoke.rs @@ -1267,7 +1267,7 @@ fn tier1_cpp_ingest_searchable() { // (method) depending on which chunk ranks first. assert!( symbol.as_deref().is_some_and(|s| s.starts_with("kebab::chunk::Foo")), - "C++ symbol must start with namespace::Class prefix, got {:?}", symbol + "C++ symbol must start with namespace::Class prefix, got {symbol:?}" ); assert!(*line_start >= 1, "line_start must be >=1"); } diff --git a/crates/kebab-app/tests/ingest_file.rs b/crates/kebab-app/tests/ingest_file.rs index 85255f6..2b9696d 100644 --- a/crates/kebab-app/tests/ingest_file.rs +++ b/crates/kebab-app/tests/ingest_file.rs @@ -33,7 +33,7 @@ fn ingest_file_copies_external_md_and_reports_new() { assert!(ext_dir.is_dir()); let entries: Vec<_> = fs::read_dir(&ext_dir) .unwrap() - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .collect(); assert_eq!(entries.len(), 1, "exactly one file in _external/"); let name = entries[0].file_name().to_string_lossy().into_owned(); diff --git a/crates/kebab-app/tests/ingest_stdin.rs b/crates/kebab-app/tests/ingest_stdin.rs index 0eeafe7..21b5c3e 100644 --- a/crates/kebab-app/tests/ingest_stdin.rs +++ b/crates/kebab-app/tests/ingest_stdin.rs @@ -35,7 +35,7 @@ fn ingest_stdin_writes_frontmatter_and_reports_new() { // _external/ contains exactly one .md file with frontmatter. let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external"); let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap() - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .collect(); assert_eq!(entries.len(), 1); let content = fs::read_to_string(entries[0].path()).unwrap(); @@ -60,7 +60,7 @@ fn ingest_stdin_without_source_uri() { let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external"); let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap() - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .collect(); let content = fs::read_to_string(entries[0].path()).unwrap(); assert!(content.contains("title: \"Title\"")); diff --git a/crates/kebab-app/tests/search_vector.rs b/crates/kebab-app/tests/search_vector.rs index 5e80e09..6fec251 100644 --- a/crates/kebab-app/tests/search_vector.rs +++ b/crates/kebab-app/tests/search_vector.rs @@ -14,12 +14,10 @@ use common::TestEnv; fn require_avx_or_panic() { #[cfg(target_arch = "x86_64")] { - if !std::is_x86_feature_detected!("avx") { - panic!( - "kb-app vector integration test requires AVX-capable hardware; \ - host CPU lacks AVX. Run on an AVX-capable machine." - ); - } + assert!(std::is_x86_feature_detected!("avx"), + "kb-app vector integration test requires AVX-capable hardware; \ + host CPU lacks AVX. Run on an AVX-capable machine." + ); } } diff --git a/crates/kebab-chunk/Cargo.toml b/crates/kebab-chunk/Cargo.toml index 923918c..67228ba 100644 --- a/crates/kebab-chunk/Cargo.toml +++ b/crates/kebab-chunk/Cargo.toml @@ -26,3 +26,6 @@ kebab-parse-code = { path = "../kebab-parse-code" } kebab-normalize = { path = "../kebab-normalize" } serde_json = { workspace = true } time = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-chunk/src/code_c_ast_v1.rs b/crates/kebab-chunk/src/code_c_ast_v1.rs index 22dbcf2..24a89b8 100644 --- a/crates/kebab-chunk/src/code_c_ast_v1.rs +++ b/crates/kebab-chunk/src/code_c_ast_v1.rs @@ -266,7 +266,7 @@ mod tests { #[test] fn oversize_unit_splits_into_parts_with_unique_ids() { - let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::>().join(""); + let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::(); let code = format!("int big() {{\n{body}\n}}"); let doc = code_doc(&[("big", 1, 502, &code)]); let chunks = CodeCAstV1Chunker.chunk(&doc, &policy()).unwrap(); @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_cpp_ast_v1.rs b/crates/kebab-chunk/src/code_cpp_ast_v1.rs index f9272d3..f69b22d 100644 --- a/crates/kebab-chunk/src/code_cpp_ast_v1.rs +++ b/crates/kebab-chunk/src/code_cpp_ast_v1.rs @@ -266,7 +266,7 @@ mod tests { #[test] fn oversize_unit_splits_into_parts_with_unique_ids() { - let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::>().join(""); + let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::(); let code = format!("int big() {{\n{body}\n}}"); let doc = code_doc(&[("big", 1, 502, &code)]); let chunks = CodeCppAstV1Chunker.chunk(&doc, &policy()).unwrap(); @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_go_ast_v1.rs b/crates/kebab-chunk/src/code_go_ast_v1.rs index 614c575..16023ee 100644 --- a/crates/kebab-chunk/src/code_go_ast_v1.rs +++ b/crates/kebab-chunk/src/code_go_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_java_ast_v1.rs b/crates/kebab-chunk/src/code_java_ast_v1.rs index 4f39658..8ebe86d 100644 --- a/crates/kebab-chunk/src/code_java_ast_v1.rs +++ b/crates/kebab-chunk/src/code_java_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_js_ast_v1.rs b/crates/kebab-chunk/src/code_js_ast_v1.rs index 7fe93cd..2019768 100644 --- a/crates/kebab-chunk/src/code_js_ast_v1.rs +++ b/crates/kebab-chunk/src/code_js_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_kotlin_ast_v1.rs b/crates/kebab-chunk/src/code_kotlin_ast_v1.rs index e1c2983..d416f5e 100644 --- a/crates/kebab-chunk/src/code_kotlin_ast_v1.rs +++ b/crates/kebab-chunk/src/code_kotlin_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_python_ast_v1.rs b/crates/kebab-chunk/src/code_python_ast_v1.rs index e814130..aa5f41d 100644 --- a/crates/kebab-chunk/src/code_python_ast_v1.rs +++ b/crates/kebab-chunk/src/code_python_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_rust_ast_v1.rs b/crates/kebab-chunk/src/code_rust_ast_v1.rs index c687c56..67a26e5 100644 --- a/crates/kebab-chunk/src/code_rust_ast_v1.rs +++ b/crates/kebab-chunk/src/code_rust_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/code_ts_ast_v1.rs b/crates/kebab-chunk/src/code_ts_ast_v1.rs index 4f273d7..97de14e 100644 --- a/crates/kebab-chunk/src/code_ts_ast_v1.rs +++ b/crates/kebab-chunk/src/code_ts_ast_v1.rs @@ -281,7 +281,7 @@ mod tests { } } let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - let n = ids.len(); ids.sort(); ids.dedup(); + let n = ids.len(); ids.sort_unstable(); ids.dedup(); assert_eq!(ids.len(), n, "chunk_ids unique across split parts"); } diff --git a/crates/kebab-chunk/src/md_heading_v1.rs b/crates/kebab-chunk/src/md_heading_v1.rs index b6094bd..7918167 100644 --- a/crates/kebab-chunk/src/md_heading_v1.rs +++ b/crates/kebab-chunk/src/md_heading_v1.rs @@ -387,9 +387,7 @@ fn render_block_text(b: &Block) -> String { // alt keeps lexical search hits on filenames working even when // P6-1's filename auto-fill is bypassed. Block::ImageRef(i) => { - let alt = if !i.alt.is_empty() { - i.alt.clone() - } else { + let alt = if i.alt.is_empty() { // P6-1 falls back to filename so this branch is // defensive — keep it lest a future test fixture or // synthetic block path skip the auto-fill. @@ -399,17 +397,17 @@ fn render_block_text(b: &Block) -> String { .filter(|s| !s.is_empty()) .unwrap_or("[image]") .to_string() + } else { + i.alt.clone() }; let ocr = i .ocr .as_ref() - .map(|o| o.joined.as_str()) - .unwrap_or(""); + .map_or("", |o| o.joined.as_str()); let cap = i .caption .as_ref() - .map(|c| c.text.as_str()) - .unwrap_or(""); + .map_or("", |c| c.text.as_str()); [alt.as_str(), ocr, cap] .iter() .filter(|s| !s.is_empty()) diff --git a/crates/kebab-chunk/src/pdf_page_v1.rs b/crates/kebab-chunk/src/pdf_page_v1.rs index 41dfe83..7935717 100644 --- a/crates/kebab-chunk/src/pdf_page_v1.rs +++ b/crates/kebab-chunk/src/pdf_page_v1.rs @@ -450,7 +450,7 @@ mod tests { // chunk_ids stay distinct despite identical block_ids — the // per-chunk policy_hash variant is doing its job. let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - ids.sort(); + ids.sort_unstable(); let total = ids.len(); ids.dedup(); assert_eq!(ids.len(), total, "all chunk_ids must be unique"); @@ -668,7 +668,7 @@ mod tests { // chunk_ids stay distinct (the per-chunk hash variant keys off // char_start which is now strictly increasing). let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); - ids.sort(); + ids.sort_unstable(); let total = ids.len(); ids.dedup(); assert_eq!(ids.len(), total, "chunk_ids must remain unique"); diff --git a/crates/kebab-chunk/tests/k8s_manifest_resource_v1.rs b/crates/kebab-chunk/tests/k8s_manifest_resource_v1.rs index 7c3e216..51f50e3 100644 --- a/crates/kebab-chunk/tests/k8s_manifest_resource_v1.rs +++ b/crates/kebab-chunk/tests/k8s_manifest_resource_v1.rs @@ -280,9 +280,7 @@ fn k8s_oversize_splits_into_line_windows_sharing_symbol() { assert_eq!( prev_end + 1, next_start, - "line ranges must be contiguous: {} → {} (got gap or overlap)", - prev_end, - next_start + "line ranges must be contiguous: {prev_end} → {next_start} (got gap or overlap)" ); } } diff --git a/crates/kebab-chunk/tests/manifest_file_v1.rs b/crates/kebab-chunk/tests/manifest_file_v1.rs index 297c563..4df5e17 100644 --- a/crates/kebab-chunk/tests/manifest_file_v1.rs +++ b/crates/kebab-chunk/tests/manifest_file_v1.rs @@ -51,7 +51,7 @@ fn manifest_doc(lang: &str, manifest_text: &str) -> CanonicalDocument { doc_id, source_asset_id: aid, workspace_path: wp, - title: format!("Manifest ({})", lang), + title: format!("Manifest ({lang})"), lang: Lang("und".into()), blocks: vec![block], metadata: Metadata { diff --git a/crates/kebab-cli/Cargo.toml b/crates/kebab-cli/Cargo.toml index 53039af..4eb9b93 100644 --- a/crates/kebab-cli/Cargo.toml +++ b/crates/kebab-cli/Cargo.toml @@ -50,3 +50,6 @@ tempfile = { workspace = true } # to simulate stale docs. `time` is the formatter used by the helper. rusqlite = { workspace = true } time = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-cli/src/main.rs b/crates/kebab-cli/src/main.rs index 5bc2139..2aaf8cf 100644 --- a/crates/kebab-cli/src/main.rs +++ b/crates/kebab-cli/src/main.rs @@ -797,7 +797,7 @@ fn run(cli: &Cli) -> anyhow::Result<()> { serde_json::to_string(&item.query)?, )?; if let Some(err) = &item.error { - writeln!(stdout, "error: {}", err)?; + writeln!(stdout, "error: {err}")?; } else if let Some(resp) = &item.response { writeln!( stdout, @@ -1171,15 +1171,13 @@ fn run(cli: &Cli) -> anyhow::Result<()> { let report = kebab_app::reset::execute(scope, &cfg)?; if cli.json { println!("{}", serde_json::to_string(&wire::wire_reset(&report))?); - } else { - if report.orphans_purged > 0 { - println!("orphans purged: {}", report.orphans_purged); - for p in &report.purged_paths { - println!(" - {}", p.0); - } - } else { - println!("no orphaned docs found — store is already in sync with walker scope"); + } else if report.orphans_purged > 0 { + println!("orphans purged: {}", report.orphans_purged); + for p in &report.purged_paths { + println!(" - {}", p.0); } + } else { + println!("no orphaned docs found — store is already in sync with walker scope"); } return Ok(()); } @@ -1508,11 +1506,11 @@ fn confirm_destructive( ) -> anyhow::Result { use std::io::Write; let mut out = std::io::stderr().lock(); - writeln!(out, "kebab reset ({:?}): about to remove", scope)?; + writeln!(out, "kebab reset ({scope:?}): about to remove")?; for p in paths { writeln!(out, " - {}", p.display())?; } - writeln!(out, "estimated total: {} bytes", bytes)?; + writeln!(out, "estimated total: {bytes} bytes")?; write!(out, "Proceed? [y/N] ")?; out.flush()?; @@ -1573,19 +1571,19 @@ fn render_fetch_plain(r: &kebab_core::FetchResult) { if !r.context_before.is_empty() { println!("\n=== before ==="); for c in &r.context_before { - let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or(""); + let heading = c.heading_path.last().map_or("", std::string::String::as_str); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text); } } if let Some(c) = &r.chunk { println!("\n=== target ==="); - let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or(""); + let heading = c.heading_path.last().map_or("", std::string::String::as_str); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text); } if !r.context_after.is_empty() { println!("\n=== after ==="); for c in &r.context_after { - let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or(""); + let heading = c.heading_path.last().map_or("", std::string::String::as_str); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text); } } diff --git a/crates/kebab-cli/src/wire.rs b/crates/kebab-cli/src/wire.rs index cf5293f..92cdcfe 100644 --- a/crates/kebab-cli/src/wire.rs +++ b/crates/kebab-cli/src/wire.rs @@ -313,7 +313,7 @@ mod tests { v.get("next_cursor").and_then(|c| c.as_str()), Some("opaque-cursor-abc") ); - assert_eq!(v.get("truncated").and_then(|t| t.as_bool()), Some(true)); + assert_eq!(v.get("truncated").and_then(serde_json::Value::as_bool), Some(true)); } #[test] diff --git a/crates/kebab-cli/tests/cli_ingest_file.rs b/crates/kebab-cli/tests/cli_ingest_file.rs index 5f81dbd..afd1a1e 100644 --- a/crates/kebab-cli/tests/cli_ingest_file.rs +++ b/crates/kebab-cli/tests/cli_ingest_file.rs @@ -88,5 +88,5 @@ max_context_tokens = 8000 let stdout = String::from_utf8_lossy(&out.stdout); let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap(); assert_eq!(v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1")); - assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1)); + assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1)); } diff --git a/crates/kebab-cli/tests/cli_ingest_stdin.rs b/crates/kebab-cli/tests/cli_ingest_stdin.rs index d040350..1c83eb5 100644 --- a/crates/kebab-cli/tests/cli_ingest_stdin.rs +++ b/crates/kebab-cli/tests/cli_ingest_stdin.rs @@ -96,5 +96,5 @@ max_context_tokens = 8000 let stdout = String::from_utf8_lossy(&out.stdout); let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap(); assert_eq!(v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1")); - assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1)); + assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1)); } diff --git a/crates/kebab-cli/tests/cli_mcp_smoke.rs b/crates/kebab-cli/tests/cli_mcp_smoke.rs index a1929c2..5e68c18 100644 --- a/crates/kebab-cli/tests/cli_mcp_smoke.rs +++ b/crates/kebab-cli/tests/cli_mcp_smoke.rs @@ -43,7 +43,7 @@ fn cli_mcp_initialize_then_tools_list() { reader.read_line(&mut line).unwrap(); let init: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); assert_eq!( - init.get("id").and_then(|i| i.as_i64()), + init.get("id").and_then(serde_json::Value::as_i64), Some(1), "unexpected id in initialize response: {init}" ); @@ -57,7 +57,7 @@ fn cli_mcp_initialize_then_tools_list() { reader.read_line(&mut line).unwrap(); let list: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); assert_eq!( - list.get("id").and_then(|i| i.as_i64()), + list.get("id").and_then(serde_json::Value::as_i64), Some(2), "unexpected id in tools/list response: {list}" ); diff --git a/crates/kebab-cli/tests/cli_schema.rs b/crates/kebab-cli/tests/cli_schema.rs index 6bc415a..f26b656 100644 --- a/crates/kebab-cli/tests/cli_schema.rs +++ b/crates/kebab-cli/tests/cli_schema.rs @@ -76,8 +76,7 @@ fn cli_schema_json_emits_schema_v1() { assert!( v.get("kebab_version") .and_then(|s| s.as_str()) - .map(|s| !s.is_empty()) - .unwrap_or(false), + .is_some_and(|s| !s.is_empty()), "kebab_version must be a non-empty string" ); @@ -86,12 +85,12 @@ fn cli_schema_json_emits_schema_v1() { .and_then(|c| c.as_object()) .expect("capabilities must be a JSON object"); assert_eq!( - caps.get("json_mode").and_then(|b| b.as_bool()), + caps.get("json_mode").and_then(serde_json::Value::as_bool), Some(true), "capabilities.json_mode must be true" ); assert_eq!( - caps.get("mcp_server").and_then(|b| b.as_bool()), + caps.get("mcp_server").and_then(serde_json::Value::as_bool), Some(true), "capabilities.mcp_server must be true (fb-30)" ); diff --git a/crates/kebab-cli/tests/ingest_progress_cli.rs b/crates/kebab-cli/tests/ingest_progress_cli.rs index df464f6..34e3b5a 100644 --- a/crates/kebab-cli/tests/ingest_progress_cli.rs +++ b/crates/kebab-cli/tests/ingest_progress_cli.rs @@ -155,8 +155,8 @@ fn ingest_json_progress_lines_carry_kind_and_ts() { saw_completed = true; // Counts mirror the report. let counts = v.get("counts").unwrap(); - assert_eq!(counts.get("scanned").and_then(|n| n.as_u64()), Some(2)); - assert_eq!(counts.get("new").and_then(|n| n.as_u64()), Some(2)); + assert_eq!(counts.get("scanned").and_then(serde_json::Value::as_u64), Some(2)); + assert_eq!(counts.get("new").and_then(serde_json::Value::as_u64), Some(2)); } } assert!(saw_scan_started, "missing scan_started event"); diff --git a/crates/kebab-config/Cargo.toml b/crates/kebab-config/Cargo.toml index 4a6e201..914f576 100644 --- a/crates/kebab-config/Cargo.toml +++ b/crates/kebab-config/Cargo.toml @@ -22,3 +22,6 @@ tracing = { workspace = true } [dev-dependencies] tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-config/src/paths.rs b/crates/kebab-config/src/paths.rs index 94db631..edf638a 100644 --- a/crates/kebab-config/src/paths.rs +++ b/crates/kebab-config/src/paths.rs @@ -157,7 +157,7 @@ mod tests { #[test] fn xdg_data_home_set_replaces_var() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let _guard = XdgGuard::capture(); // SAFETY: lock held for the duration of this test. unsafe { std::env::set_var("XDG_DATA_HOME", "/custom/path") }; @@ -168,7 +168,7 @@ mod tests { #[test] fn xdg_data_home_unset_uses_default() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let _guard = XdgGuard::capture(); // SAFETY: lock held for the duration of this test. unsafe { std::env::remove_var("XDG_DATA_HOME") }; @@ -181,7 +181,7 @@ mod tests { #[test] fn xdg_with_no_default_resolves_to_empty_when_unset() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let _guard = XdgGuard::capture(); // SAFETY: lock held for the duration of this test. unsafe { std::env::remove_var("XDG_DATA_HOME") }; @@ -193,7 +193,7 @@ mod tests { #[test] fn leading_tilde_expands_to_home() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let home = std::env::var("HOME").expect("HOME must be set in tests"); let p = expand_path("~/runs", ""); assert_eq!(p, PathBuf::from(home).join("runs")); @@ -229,7 +229,7 @@ mod tests { #[test] fn tilde_path_ignores_base_dir() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let home = std::env::var("HOME").expect("HOME must be set in tests"); let base = Path::new("/tmp/ignored-cfg"); let p = expand_path_with_base("~/x", "", base); @@ -238,7 +238,7 @@ mod tests { #[test] fn xdg_var_path_ignores_base_dir() { - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let _guard = XdgGuard::capture(); // SAFETY: lock held for the duration of this test. unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") }; @@ -255,7 +255,7 @@ mod tests { // Order matters: substitute `{data_dir}` (which itself contains // an unexpanded `${XDG_DATA_HOME}` and `~`), then the other two // resolve the result. - let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let _guard = XdgGuard::capture(); // SAFETY: lock held for the duration of this test. unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") }; diff --git a/crates/kebab-core/Cargo.toml b/crates/kebab-core/Cargo.toml index 38b50af..5b9c603 100644 --- a/crates/kebab-core/Cargo.toml +++ b/crates/kebab-core/Cargo.toml @@ -16,3 +16,6 @@ time = { workspace = true } blake3 = { workspace = true } serde_json_canonicalizer = "0.3" unicode-normalization = "0.1" + +[lints] +workspace = true diff --git a/crates/kebab-core/src/citation.rs b/crates/kebab-core/src/citation.rs index a27b28d..a73f74f 100644 --- a/crates/kebab-core/src/citation.rs +++ b/crates/kebab-core/src/citation.rs @@ -226,28 +226,25 @@ fn parse_hms_ms(s: &str) -> Result { let m: u64 = parts[1] .parse() .map_err(|_| anyhow::anyhow!("bad minutes in {:?} (input {s:?})", parts[1]))?; - let (sec, ms) = match parts[2].split_once('.') { - Some((s_part, ms_part)) => { - let sec: u64 = s_part - .parse() - .map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?; - // Pad/truncate to exactly 3 digits. - let mut ms_str = ms_part.to_owned(); - while ms_str.len() < 3 { - ms_str.push('0'); - } - ms_str.truncate(3); - let ms: u64 = ms_str - .parse() - .map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?; - (sec, ms) - } - None => { - let sec: u64 = parts[2] - .parse() - .map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?; - (sec, 0) + let (sec, ms) = if let Some((s_part, ms_part)) = parts[2].split_once('.') { + let sec: u64 = s_part + .parse() + .map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?; + // Pad/truncate to exactly 3 digits. + let mut ms_str = ms_part.to_owned(); + while ms_str.len() < 3 { + ms_str.push('0'); } + ms_str.truncate(3); + let ms: u64 = ms_str + .parse() + .map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?; + (sec, ms) + } else { + let sec: u64 = parts[2] + .parse() + .map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?; + (sec, 0) }; Ok(h * 3_600_000 + m * 60_000 + sec * 1000 + ms) } diff --git a/crates/kebab-core/src/search.rs b/crates/kebab-core/src/search.rs index eaf8470..8c6e6e0 100644 --- a/crates/kebab-core/src/search.rs +++ b/crates/kebab-core/src/search.rs @@ -471,7 +471,7 @@ mod tests { doc_path: WorkspacePath("a.md".into()), heading_path: vec![], section_label: None, - snippet: "".into(), + snippet: String::new(), citation: Citation::Line { path: WorkspacePath("a.md".into()), start: 1, @@ -502,7 +502,7 @@ mod tests { doc_path: WorkspacePath("a.rs".into()), heading_path: vec![], section_label: None, - snippet: "".into(), + snippet: String::new(), citation: Citation::Code { path: WorkspacePath("a.rs".into()), line_start: 1, diff --git a/crates/kebab-embed-local/Cargo.toml b/crates/kebab-embed-local/Cargo.toml index 452b2f1..b0b2085 100644 --- a/crates/kebab-embed-local/Cargo.toml +++ b/crates/kebab-embed-local/Cargo.toml @@ -20,3 +20,6 @@ anyhow = { workspace = true } [dev-dependencies] tempfile = { workspace = true } serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-embed-local/src/lib.rs b/crates/kebab-embed-local/src/lib.rs index 2033515..91253d0 100644 --- a/crates/kebab-embed-local/src/lib.rs +++ b/crates/kebab-embed-local/src/lib.rs @@ -158,7 +158,7 @@ impl Embedder for FastembedEmbedder { let guard = self .inner .lock() - .unwrap_or_else(|p| p.into_inner()); + .unwrap_or_else(std::sync::PoisonError::into_inner); let batch: Vec> = guard .embed(chunk_vec, Some(self.batch_size)) .context("fastembed: embed")?; diff --git a/crates/kebab-embed/Cargo.toml b/crates/kebab-embed/Cargo.toml index 451607b..e16c144 100644 --- a/crates/kebab-embed/Cargo.toml +++ b/crates/kebab-embed/Cargo.toml @@ -28,3 +28,6 @@ mock = [] [dev-dependencies] proptest = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-embed/src/lib.rs b/crates/kebab-embed/src/lib.rs index 6eaea68..077a872 100644 --- a/crates/kebab-embed/src/lib.rs +++ b/crates/kebab-embed/src/lib.rs @@ -59,7 +59,7 @@ pub fn assert_vector_shape(vecs: &[Vec], expected_dims: usize) { /// Panics on mismatch (test-only helper — callers are tests). pub fn assert_unit_norm(vecs: &[Vec], tolerance: f32) { for (i, v) in vecs.iter().enumerate() { - let norm_sq: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum(); + let norm_sq: f64 = v.iter().map(|&x| f64::from(x) * f64::from(x)).sum(); let norm = norm_sq.sqrt() as f32; assert!( (norm - 1.0).abs() <= tolerance, diff --git a/crates/kebab-embed/src/mock.rs b/crates/kebab-embed/src/mock.rs index d538bc3..a458562 100644 --- a/crates/kebab-embed/src/mock.rs +++ b/crates/kebab-embed/src/mock.rs @@ -132,10 +132,10 @@ impl Embedder for MockEmbedder { .collect(); // L2-normalize. Skip the rare all-zero case to avoid 0/0 = NaN. - let norm_sq: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum(); + let norm_sq: f64 = v.iter().map(|&x| f64::from(x) * f64::from(x)).sum(); if norm_sq > 0.0 { let inv = (1.0 / norm_sq.sqrt()) as f32; - for x in v.iter_mut() { + for x in &mut v { *x *= inv; } } diff --git a/crates/kebab-eval/Cargo.toml b/crates/kebab-eval/Cargo.toml index 137d058..ab3ab44 100644 --- a/crates/kebab-eval/Cargo.toml +++ b/crates/kebab-eval/Cargo.toml @@ -28,3 +28,6 @@ uuid = { workspace = true } [dev-dependencies] tempfile = { workspace = true } rusqlite = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-eval/src/compare.rs b/crates/kebab-eval/src/compare.rs index 5033ca7..606d00d 100644 --- a/crates/kebab-eval/src/compare.rs +++ b/crates/kebab-eval/src/compare.rs @@ -260,8 +260,8 @@ pub fn render_report_md(report: &CompareReport) -> String { "| {} | {} | {} | {} | {} |", c.query_id, comparison_kind_label(c.kind), - c.a_hit_rank.map(|r| r.to_string()).unwrap_or_else(|| "—".into()), - c.b_hit_rank.map(|r| r.to_string()).unwrap_or_else(|| "—".into()), + c.a_hit_rank.map_or_else(|| "—".into(), |r| r.to_string()), + c.b_hit_rank.map_or_else(|| "—".into(), |r| r.to_string()), c.note.as_deref().unwrap_or(""), ); } @@ -308,7 +308,7 @@ fn extract_chunker_version(snapshot_json: &str) -> Option { let v: serde_json::Value = serde_json::from_str(snapshot_json).ok()?; v.get("chunker_version") .and_then(|x| x.as_str()) - .map(|s| s.to_owned()) + .map(std::borrow::ToOwned::to_owned) } fn parse_results( @@ -402,8 +402,7 @@ fn classify( // so refusal-flow queries (no expected_*) don't appear as // regressions. let has_expected = gq - .map(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty()) - .unwrap_or(false); + .is_some_and(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty()); if has_expected { (ComparisonKind::Regression, Some("hit→miss".into())) } else { @@ -426,7 +425,7 @@ fn build_deltas( if a.is_nan() || b.is_nan() { serde_json::Value::Null } else { - serde_json::Value::from((b - a) as f64) + serde_json::Value::from(f64::from(b - a)) } } let mut hit = serde_json::Map::new(); diff --git a/crates/kebab-eval/src/metrics.rs b/crates/kebab-eval/src/metrics.rs index d9565a8..2a6ad41 100644 --- a/crates/kebab-eval/src/metrics.rs +++ b/crates/kebab-eval/src/metrics.rs @@ -270,7 +270,21 @@ pub(crate) fn aggregate_from_rows( // recall@k_doc (doc-level, requires non-empty expected_doc_ids // and `>0` is the "should retrieve" condition; refusal queries // (`expected_doc_ids = []`) are excluded by spec). - if !gq.expected_doc_ids.is_empty() { + if gq.expected_doc_ids.is_empty() { + // refusal_correctness: golden marks "should refuse" via empty + // expected_doc_ids. We can only judge this on RAG runs — a + // lexical-only run produces no Answer, so "refusal" is + // undefined. Excluding such queries from the denominator + // (rather than counting them as failures) keeps the metric + // honest: a search-only run reports refusal_correctness as + // NaN/null, not 0.0. + if let Some(ans) = &qr.answer { + refusal_denom += 1; + if !ans.grounded { + refusal_num += 1; + } + } + } else { let expected_docs: HashSet<&DocumentId> = gq.expected_doc_ids.iter().collect(); for k in TOP_K_VARIANTS { let entry = recall_at_k_doc.get_mut(k).expect("init"); @@ -285,20 +299,6 @@ pub(crate) fn aggregate_from_rows( let frac = covered as f64 / expected_docs.len() as f64; entry.0 += frac; } - } else { - // refusal_correctness: golden marks "should refuse" via empty - // expected_doc_ids. We can only judge this on RAG runs — a - // lexical-only run produces no Answer, so "refusal" is - // undefined. Excluding such queries from the denominator - // (rather than counting them as failures) keeps the metric - // honest: a search-only run reports refusal_correctness as - // NaN/null, not 0.0. - if let Some(ans) = &qr.answer { - refusal_denom += 1; - if !ans.grounded { - refusal_num += 1; - } - } } // groundedness + citation_coverage (only meaningful with RAG diff --git a/crates/kebab-eval/tests/metrics_and_compare.rs b/crates/kebab-eval/tests/metrics_and_compare.rs index 17b6e56..9110e60 100644 --- a/crates/kebab-eval/tests/metrics_and_compare.rs +++ b/crates/kebab-eval/tests/metrics_and_compare.rs @@ -143,7 +143,7 @@ fn env_guard() -> std::sync::MutexGuard<'static, ()> { static M: OnceLock> = OnceLock::new(); M.get_or_init(|| Mutex::new(())) .lock() - .unwrap_or_else(|e| e.into_inner()) + .unwrap_or_else(std::sync::PoisonError::into_inner) } #[test] diff --git a/crates/kebab-eval/tests/runner.rs b/crates/kebab-eval/tests/runner.rs index 29b3830..9f26c3e 100644 --- a/crates/kebab-eval/tests/runner.rs +++ b/crates/kebab-eval/tests/runner.rs @@ -147,7 +147,7 @@ fn lexical_opts() -> EvalRunOpts { /// guard must outlive the call so concurrent tests don't reset the /// var mid-run. fn run_with_golden R, R>(yaml: &Path, f: F) -> R { - let _g = GOLDEN_ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner()); + let _g = GOLDEN_ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner); // SAFETY: `KEBAB_EVAL_GOLDEN` is a benign env var; the GOLDEN_ENV_LOCK // serializes mutations so concurrent tests don't race. unsafe { diff --git a/crates/kebab-llm-local/Cargo.toml b/crates/kebab-llm-local/Cargo.toml index 6cc8669..1db5299 100644 --- a/crates/kebab-llm-local/Cargo.toml +++ b/crates/kebab-llm-local/Cargo.toml @@ -34,3 +34,6 @@ anyhow = { workspace = true } # `tokio::*` symbols, so the public/runtime API stays sync. wiremock = { workspace = true } tokio = { workspace = true, features = ["macros", "rt"] } + +[lints] +workspace = true diff --git a/crates/kebab-llm-local/src/ollama.rs b/crates/kebab-llm-local/src/ollama.rs index 8955adc..3e797cf 100644 --- a/crates/kebab-llm-local/src/ollama.rs +++ b/crates/kebab-llm-local/src/ollama.rs @@ -400,9 +400,9 @@ impl Iterator for OllamaStream { // u32 saturation: even ~4G tokens is implausible for a // single chat turn; we still saturate rather than // panic on the unlikely case. - prompt_tokens: prompt_tokens.min(u32::MAX as u64) as u32, - completion_tokens: completion_tokens.min(u32::MAX as u64) as u32, - latency_ms: (total_duration_ns / 1_000_000).min(u32::MAX as u64) as u32, + prompt_tokens: prompt_tokens.min(u64::from(u32::MAX)) as u32, + completion_tokens: completion_tokens.min(u64::from(u32::MAX)) as u32, + latency_ms: (total_duration_ns / 1_000_000).min(u64::from(u32::MAX)) as u32, }; return Some(Ok(TokenChunk::Done { finish_reason, diff --git a/crates/kebab-llm/Cargo.toml b/crates/kebab-llm/Cargo.toml index 15684b5..2a0252a 100644 --- a/crates/kebab-llm/Cargo.toml +++ b/crates/kebab-llm/Cargo.toml @@ -19,3 +19,6 @@ mock = [] [dev-dependencies] proptest = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-mcp/Cargo.toml b/crates/kebab-mcp/Cargo.toml index 9ecea0d..489cd65 100644 --- a/crates/kebab-mcp/Cargo.toml +++ b/crates/kebab-mcp/Cargo.toml @@ -27,3 +27,6 @@ kebab-core = { path = "../kebab-core" } [dev-dependencies] tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-mcp/tests/tools_call_ask.rs b/crates/kebab-mcp/tests/tools_call_ask.rs index 0a335cb..641e4bd 100644 --- a/crates/kebab-mcp/tests/tools_call_ask.rs +++ b/crates/kebab-mcp/tests/tools_call_ask.rs @@ -65,8 +65,7 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() { // Empty KB → refusal (grounded:false) is normal — NOT isError. assert!( !result.is_error.unwrap_or(false), - "expected isError=false on refusal, got {:?}", - result + "expected isError=false on refusal, got {result:?}" ); let content = result @@ -86,7 +85,7 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() { "response should carry schema_version=answer.v1" ); assert_eq!( - v.get("grounded").and_then(|b| b.as_bool()), + v.get("grounded").and_then(serde_json::Value::as_bool), Some(false), "empty KB should produce grounded=false" ); diff --git a/crates/kebab-mcp/tests/tools_call_doctor.rs b/crates/kebab-mcp/tests/tools_call_doctor.rs index e7bde10..a7eb53e 100644 --- a/crates/kebab-mcp/tests/tools_call_doctor.rs +++ b/crates/kebab-mcp/tests/tools_call_doctor.rs @@ -44,7 +44,7 @@ async fn doctor_tool_returns_doctor_v1_json() { // `ok` boolean must be present (value may be false in CI where Ollama // is not reachable — that's expected and acceptable). assert!( - v.get("ok").and_then(|b| b.as_bool()).is_some(), + v.get("ok").and_then(serde_json::Value::as_bool).is_some(), "`ok` field missing in doctor.v1 response: {v}" ); } diff --git a/crates/kebab-mcp/tests/tools_call_fetch.rs b/crates/kebab-mcp/tests/tools_call_fetch.rs index 821db4d..2810218 100644 --- a/crates/kebab-mcp/tests/tools_call_fetch.rs +++ b/crates/kebab-mcp/tests/tools_call_fetch.rs @@ -98,8 +98,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() { assert!( !result.is_error.unwrap_or(false), - "expected isError=false, got {:?}", - result + "expected isError=false, got {result:?}" ); let content = result @@ -123,7 +122,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() { "kind must be 'chunk'" ); assert!( - v.get("chunk").is_some_and(|c| c.is_object()), + v.get("chunk").is_some_and(serde_json::Value::is_object), "chunk payload must be populated for kind=chunk" ); } diff --git a/crates/kebab-mcp/tests/tools_call_ingest_file.rs b/crates/kebab-mcp/tests/tools_call_ingest_file.rs index ff112b8..43a9bd1 100644 --- a/crates/kebab-mcp/tests/tools_call_ingest_file.rs +++ b/crates/kebab-mcp/tests/tools_call_ingest_file.rs @@ -49,7 +49,7 @@ async fn ingest_file_tool_returns_ingest_report_v1() { v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1") ); - assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1)); + assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1)); } #[tokio::test] @@ -91,7 +91,7 @@ async fn ingest_file_tool_idempotent_on_second_call() { other => panic!("expected text, got {other:?}"), }; let v1: serde_json::Value = serde_json::from_str(text1).unwrap(); - assert_eq!(v1.get("new").and_then(|n| n.as_u64()), Some(1)); + assert_eq!(v1.get("new").and_then(serde_json::Value::as_u64), Some(1)); // Second call — same content, expect unchanged=1. let r2 = tokio::task::spawn_blocking({ @@ -112,6 +112,6 @@ async fn ingest_file_tool_idempotent_on_second_call() { other => panic!("expected text, got {other:?}"), }; let v2: serde_json::Value = serde_json::from_str(text2).unwrap(); - assert_eq!(v2.get("new").and_then(|n| n.as_u64()), Some(0), "{v2:?}"); - assert_eq!(v2.get("unchanged").and_then(|n| n.as_u64()), Some(1), "{v2:?}"); + assert_eq!(v2.get("new").and_then(serde_json::Value::as_u64), Some(0), "{v2:?}"); + assert_eq!(v2.get("unchanged").and_then(serde_json::Value::as_u64), Some(1), "{v2:?}"); } diff --git a/crates/kebab-mcp/tests/tools_call_ingest_stdin.rs b/crates/kebab-mcp/tests/tools_call_ingest_stdin.rs index 45943d6..0e9967e 100644 --- a/crates/kebab-mcp/tests/tools_call_ingest_stdin.rs +++ b/crates/kebab-mcp/tests/tools_call_ingest_stdin.rs @@ -52,7 +52,7 @@ async fn ingest_stdin_tool_returns_ingest_report_v1() { v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1") ); - assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1)); + assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1)); } #[tokio::test] diff --git a/crates/kebab-mcp/tests/tools_call_schema.rs b/crates/kebab-mcp/tests/tools_call_schema.rs index c47a874..eccdf0f 100644 --- a/crates/kebab-mcp/tests/tools_call_schema.rs +++ b/crates/kebab-mcp/tests/tools_call_schema.rs @@ -49,8 +49,7 @@ async fn schema_tool_returns_schema_v1_json() { assert!( !result.is_error.unwrap_or(false), - "expected isError=false on healthy schema, got {:?}", - result + "expected isError=false on healthy schema, got {result:?}" ); let content = result.content.first().expect("expected at least one content item"); @@ -68,7 +67,7 @@ async fn schema_tool_returns_schema_v1_json() { "unexpected schema_version in: {v}" ); assert_eq!( - v.get("capabilities").and_then(|c| c.get("mcp_server")).and_then(|b| b.as_bool()), + v.get("capabilities").and_then(|c| c.get("mcp_server")).and_then(serde_json::Value::as_bool), Some(true), "mcp_server capability flag should be true after fb-30", ); diff --git a/crates/kebab-mcp/tests/tools_call_search.rs b/crates/kebab-mcp/tests/tools_call_search.rs index 58456f7..5152fce 100644 --- a/crates/kebab-mcp/tests/tools_call_search.rs +++ b/crates/kebab-mcp/tests/tools_call_search.rs @@ -71,8 +71,7 @@ async fn search_tool_returns_search_response_v1() { assert!( !result.is_error.unwrap_or(false), - "expected isError=false, got {:?}", - result + "expected isError=false, got {result:?}" ); let content = result @@ -108,7 +107,7 @@ async fn search_tool_returns_search_response_v1() { ); // truncated must be present (bool); next_cursor may be null on last page. assert!( - v.get("truncated").and_then(|t| t.as_bool()).is_some(), + v.get("truncated").and_then(serde_json::Value::as_bool).is_some(), "envelope should carry truncated:bool" ); assert!( @@ -172,8 +171,7 @@ async fn search_with_doc_id_filter_returns_only_target() { ); assert!( !unfiltered.is_error.unwrap_or(false), - "unfiltered search failed: {:?}", - unfiltered + "unfiltered search failed: {unfiltered:?}" ); let unfiltered_text = match &unfiltered.content.first().unwrap().raw { RawContent::Text(t) => t.text.clone(), @@ -211,8 +209,7 @@ async fn search_with_doc_id_filter_returns_only_target() { ); assert!( !filtered.is_error.unwrap_or(false), - "filtered search failed: {:?}", - filtered + "filtered search failed: {filtered:?}" ); let filtered_text = match &filtered.content.first().unwrap().raw { RawContent::Text(t) => t.text.clone(), diff --git a/crates/kebab-nli/Cargo.toml b/crates/kebab-nli/Cargo.toml index 9886bca..00e548d 100644 --- a/crates/kebab-nli/Cargo.toml +++ b/crates/kebab-nli/Cargo.toml @@ -28,3 +28,6 @@ tracing = { workspace = true } [dev-dependencies] tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-nli/src/onnx.rs b/crates/kebab-nli/src/onnx.rs index ec49002..d2d2382 100644 --- a/crates/kebab-nli/src/onnx.rs +++ b/crates/kebab-nli/src/onnx.rs @@ -235,11 +235,11 @@ impl NliVerifier for OnnxNliVerifier { .encode((premise, hypothesis), true) .map_err(|e| anyhow!("kebab-nli: tokenizer.encode failed: {e}"))?; - let ids: Vec = enc.get_ids().iter().map(|&u| u as i64).collect(); + let ids: Vec = enc.get_ids().iter().map(|&u| i64::from(u)).collect(); let mask: Vec = enc .get_attention_mask() .iter() - .map(|&u| u as i64) + .map(|&u| i64::from(u)) .collect(); let seq_len = ids.len(); @@ -266,8 +266,7 @@ impl NliVerifier for OnnxNliVerifier { let shape = logits.shape(); if shape != [1, LOGITS_LEN] { anyhow::bail!( - "kebab-nli: unexpected logits shape {:?}, expected [1, {LOGITS_LEN}]", - shape + "kebab-nli: unexpected logits shape {shape:?}, expected [1, {LOGITS_LEN}]" ); } let l = [logits[[0, 0]], logits[[0, 1]], logits[[0, 2]]]; diff --git a/crates/kebab-nli/tests/inference.rs b/crates/kebab-nli/tests/inference.rs index fc751c1..4115199 100644 --- a/crates/kebab-nli/tests/inference.rs +++ b/crates/kebab-nli/tests/inference.rs @@ -111,8 +111,7 @@ fn long_premise_truncates_without_panic() { ] { assert!( x.is_finite(), - "channel {name} non-finite: {x} (full scores: {:?})", - s + "channel {name} non-finite: {x} (full scores: {s:?})" ); } // Softmax invariant — the three channels sum to ~1. diff --git a/crates/kebab-normalize/Cargo.toml b/crates/kebab-normalize/Cargo.toml index 9138837..493c7b0 100644 --- a/crates/kebab-normalize/Cargo.toml +++ b/crates/kebab-normalize/Cargo.toml @@ -25,3 +25,6 @@ tracing = { workspace = true } # default scope, excluding dev-deps) confirms this. kebab-parse-md = { path = "../kebab-parse-md" } serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-parse-code/Cargo.toml b/crates/kebab-parse-code/Cargo.toml index cfdbca6..979c804 100644 --- a/crates/kebab-parse-code/Cargo.toml +++ b/crates/kebab-parse-code/Cargo.toml @@ -27,3 +27,6 @@ tree-sitter-cpp = { workspace = true } [dev-dependencies] tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-parse-code/src/c.rs b/crates/kebab-parse-code/src/c.rs index ce7e1a3..0ff2114 100644 --- a/crates/kebab-parse-code/src/c.rs +++ b/crates/kebab-parse-code/src/c.rs @@ -310,7 +310,7 @@ fn build_blocks( // If there is only glue (no real unit) the single pushed "" // label should be "" — rename it now. if !has_real_unit { - for (sym, _, _, _) in units.iter_mut() { + for (sym, _, _, _) in &mut units { if sym == "" { *sym = "".to_string(); } @@ -329,7 +329,7 @@ fn build_blocks( lang: Some("c".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, @@ -704,11 +704,11 @@ void print_result(int v) { #[test] fn c_extractor_deterministic_across_runs() { - let src = r#" + let src = r" struct Node { int val; }; int sum(int a, int b) { return a + b; } void noop(void) {} -"#; +"; let a = tests_support::extract_c(src, "x/det.c"); for _ in 0..20 { assert_eq!( diff --git a/crates/kebab-parse-code/src/cpp.rs b/crates/kebab-parse-code/src/cpp.rs index 81bf1f9..3661df1 100644 --- a/crates/kebab-parse-code/src/cpp.rs +++ b/crates/kebab-parse-code/src/cpp.rs @@ -224,7 +224,7 @@ fn build_blocks_top( units.push(("".to_string(), 1, total.max(1), false)); } if !has_real_unit { - for (sym, _, _, _) in units.iter_mut() { + for (sym, _, _, _) in &mut units { if sym == "" { *sym = "".to_string(); } @@ -243,7 +243,7 @@ fn build_blocks_top( lang: Some("cpp".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, @@ -696,7 +696,7 @@ mod tests { #[test] fn namespace_and_class() { - let src = r#" + let src = r" namespace ns { class Foo { public: @@ -706,7 +706,7 @@ namespace ns { int operator+(const Foo& o) { return 0; } }; } -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!(s.iter().any(|x| x == "ns::Foo"), "ns::Foo missing: {s:?}"); @@ -718,11 +718,11 @@ namespace ns { #[test] fn anonymous_namespace() { - let src = r#" + let src = r" namespace { void hidden_fn() {} } -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!( @@ -733,11 +733,11 @@ namespace { #[test] fn nested_namespace_specifier() { - let src = r#" + let src = r" namespace outer::inner { void fn_in_nested() {} } -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!( @@ -748,9 +748,9 @@ namespace outer::inner { #[test] fn out_of_class_method_def() { - let src = r#" + let src = r" void ns::Foo::method() { } -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!( @@ -761,7 +761,7 @@ void ns::Foo::method() { } #[test] fn template_declaration() { - let src = r#" + let src = r" template class Bar { void tmpl_method() {} @@ -769,7 +769,7 @@ class Bar { template void tmpl_free_fn(T x) {} -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!(s.iter().any(|x| x == "Bar"), "Bar class missing: {s:?}"); @@ -785,12 +785,12 @@ void tmpl_free_fn(T x) {} #[test] fn enum_and_concept() { - let src = r#" + let src = r" enum class Color { Red, Green }; template concept Printable = requires(T t) { t.print(); }; -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!(s.iter().any(|x| x == "Color"), "Color missing: {s:?}"); @@ -813,11 +813,11 @@ extern "C" { #[test] fn conversion_operator() { - let src = r#" + let src = r" class Foo { operator bool() const { return true; } }; -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!( @@ -852,11 +852,11 @@ class Foo { #[test] fn ref_returning_operator() { - let src = r#" + let src = r" class Foo { Foo& operator=(const Foo& o) { return *this; } }; -"#; +"; let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let s = syms(&doc); assert!( @@ -867,14 +867,14 @@ class Foo { #[test] fn deterministic_across_runs() { - let src = r#" + let src = r" namespace ns { class Foo { void method() {} }; } void free_fn() {} -"#; +"; let a = tests_support::extract_cpp(src, "x/foo.cpp"); for _ in 0..20 { assert_eq!(tests_support::extract_cpp(src, "x/foo.cpp").blocks, a.blocks); diff --git a/crates/kebab-parse-code/src/go.rs b/crates/kebab-parse-code/src/go.rs index 7ff8eba..76a9d87 100644 --- a/crates/kebab-parse-code/src/go.rs +++ b/crates/kebab-parse-code/src/go.rs @@ -315,7 +315,7 @@ fn build_blocks( // mod-prefix-agnostic. let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -335,7 +335,7 @@ fn build_blocks( lang: Some("go".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/java.rs b/crates/kebab-parse-code/src/java.rs index c9eaeb7..9cf6604 100644 --- a/crates/kebab-parse-code/src/java.rs +++ b/crates/kebab-parse-code/src/java.rs @@ -248,7 +248,7 @@ fn build_blocks( // post-pass as 1B / 1C-Go). let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -268,7 +268,7 @@ fn build_blocks( lang: Some("java".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/javascript.rs b/crates/kebab-parse-code/src/javascript.rs index f2e2a16..88301e7 100644 --- a/crates/kebab-parse-code/src/javascript.rs +++ b/crates/kebab-parse-code/src/javascript.rs @@ -293,7 +293,7 @@ fn build_blocks( let inner_kind = inner.kind(); match inner_kind { "function_declaration" | "class_declaration" => { - let name_opt = name_text(&inner, src).map(|s| s.to_string()); + let name_opt = name_text(&inner, src).map(std::string::ToString::to_string); if let Some(name) = name_opt { glue.retain(|(_, gs, _)| *gs < outer_s); flush_glue(glue, units, mod_prefix, mod_path); @@ -332,7 +332,7 @@ fn build_blocks( | "function_declaration" | "class" | "class_declaration" => { - let name_opt = name_text(&value, src).map(|s| s.to_string()); + let name_opt = name_text(&value, src).map(std::string::ToString::to_string); let leaf = name_opt.as_deref().unwrap_or("default").to_string(); glue.retain(|(_, gs, _)| *gs < outer_s); @@ -402,7 +402,7 @@ fn build_blocks( // post-pass as 1A Gap 1 / Python / TS). let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -422,7 +422,7 @@ fn build_blocks( lang: Some("javascript".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/kotlin.rs b/crates/kebab-parse-code/src/kotlin.rs index 0e6d5d3..5db947c 100644 --- a/crates/kebab-parse-code/src/kotlin.rs +++ b/crates/kebab-parse-code/src/kotlin.rs @@ -290,7 +290,7 @@ fn build_blocks( // post-pass as 1B / 1C-Go / Java). let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -310,7 +310,7 @@ fn build_blocks( lang: Some("kotlin".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/python.rs b/crates/kebab-parse-code/src/python.rs index e2b1ae7..4959901 100644 --- a/crates/kebab-parse-code/src/python.rs +++ b/crates/kebab-parse-code/src/python.rs @@ -333,7 +333,7 @@ fn build_blocks( // future-proofed) still demotes correctly. let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -353,7 +353,7 @@ fn build_blocks( lang: Some("python".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/rust.rs b/crates/kebab-parse-code/src/rust.rs index 4b932a6..29b36c7 100644 --- a/crates/kebab-parse-code/src/rust.rs +++ b/crates/kebab-parse-code/src/rust.rs @@ -336,7 +336,7 @@ fn build_blocks( // group is ``, even a pure mod-decl group. let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { // Match on the *suffix*: a glue group may now carry a module // prefix (`inner::`), so demote any `…` to the // same-prefixed `…` rather than only the bare form. @@ -359,7 +359,7 @@ fn build_blocks( lang: Some("rust".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-code/src/typescript.rs b/crates/kebab-parse-code/src/typescript.rs index 82232d6..706eb1d 100644 --- a/crates/kebab-parse-code/src/typescript.rs +++ b/crates/kebab-parse-code/src/typescript.rs @@ -326,7 +326,7 @@ fn build_blocks( | "interface_declaration" | "type_alias_declaration" | "enum_declaration" => { - let name_opt = name_text(&inner, src).map(|s| s.to_string()); + let name_opt = name_text(&inner, src).map(std::string::ToString::to_string); if let Some(name) = name_opt { glue.retain(|(_, gs, _)| *gs < outer_s); flush_glue(glue, units, mod_prefix, mod_path); @@ -376,7 +376,7 @@ fn build_blocks( | "class" | "class_declaration" => { let name_opt = - name_text(&value, src).map(|s| s.to_string()); + name_text(&value, src).map(std::string::ToString::to_string); let leaf = name_opt .as_deref() .unwrap_or("default") @@ -461,7 +461,7 @@ fn build_blocks( // post-pass as 1A Gap 1 / Python). let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); if has_real_unit { - for (sym, _, _, is_real) in units.iter_mut() { + for (sym, _, _, is_real) in &mut units { if !*is_real && sym.ends_with("") { let pre = &sym[..sym.len() - "".len()]; *sym = format!("{pre}"); @@ -481,7 +481,7 @@ fn build_blocks( lang: Some("typescript".to_string()), }; let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); - let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n"); + let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n"); blocks.push(Block::Code(CodeBlock { common: CommonBlock { block_id, diff --git a/crates/kebab-parse-image/Cargo.toml b/crates/kebab-parse-image/Cargo.toml index 46b56bc..eeacbfc 100644 --- a/crates/kebab-parse-image/Cargo.toml +++ b/crates/kebab-parse-image/Cargo.toml @@ -55,3 +55,6 @@ base64 = { workspace = true } # at runtime) is preserved. kebab-llm = { path = "../kebab-llm", features = ["mock"] } kebab-llm-local = { path = "../kebab-llm-local" } + +[lints] +workspace = true diff --git a/crates/kebab-parse-image/src/caption.rs b/crates/kebab-parse-image/src/caption.rs index 23b4a1b..d08ff55 100644 --- a/crates/kebab-parse-image/src/caption.rs +++ b/crates/kebab-parse-image/src/caption.rs @@ -198,7 +198,7 @@ pub fn apply_caption( /// language; everything else falls through to English. fn build_prompt(lang_hint: Option<&str>) -> (String, String) { match lang_hint { - Some("ko") | Some("kor") => ( + Some("ko" | "kor") => ( "이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \ 보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \ 한 문장만 출력." diff --git a/crates/kebab-parse-image/src/exif_extract.rs b/crates/kebab-parse-image/src/exif_extract.rs index 3560b1f..4e5253c 100644 --- a/crates/kebab-parse-image/src/exif_extract.rs +++ b/crates/kebab-parse-image/src/exif_extract.rs @@ -103,7 +103,7 @@ fn ascii_field(exif: &exif::Exif, tag: Tag) -> Option { fn u32_field(exif: &exif::Exif, tag: Tag) -> Option { let f = exif.get_field(tag, In::PRIMARY)?; match &f.value { - Value::Short(v) => v.first().map(|x| *x as u32), + Value::Short(v) => v.first().map(|x| u32::from(*x)), Value::Long(v) => v.first().copied(), _ => None, } @@ -177,7 +177,7 @@ fn rational_to_f64(r: &exif::Rational) -> Option { if r.denom == 0 { None } else { - Some(r.num as f64 / r.denom as f64) + Some(f64::from(r.num) / f64::from(r.denom)) } } diff --git a/crates/kebab-parse-image/src/image_prep.rs b/crates/kebab-parse-image/src/image_prep.rs index 6fa7cfd..e26703d 100644 --- a/crates/kebab-parse-image/src/image_prep.rs +++ b/crates/kebab-parse-image/src/image_prep.rs @@ -162,9 +162,7 @@ mod tests { let ratio = w as f32 / h as f32; assert!( (ratio - 4.0 / 3.0).abs() < 0.02, - "aspect drift: in=4/3 out={}/{}={ratio}", - w, - h + "aspect drift: in=4/3 out={w}/{h}={ratio}" ); } diff --git a/crates/kebab-parse-image/tests/common/mod.rs b/crates/kebab-parse-image/tests/common/mod.rs index 7c8e8e0..1cb502a 100644 --- a/crates/kebab-parse-image/tests/common/mod.rs +++ b/crates/kebab-parse-image/tests/common/mod.rs @@ -142,7 +142,7 @@ fn splice_exif_into_jpeg(exif_blob: Vec) -> Vec { // + exif_blob.len(). Pre-validated against the 0xFFFF segment limit. let app1_payload_len = 2 + 6 + exif_blob.len(); assert!( - app1_payload_len <= u16::MAX as usize, + u16::try_from(app1_payload_len).is_ok(), "EXIF segment too large for a single APP1" ); out.extend_from_slice(&(app1_payload_len as u16).to_be_bytes()); diff --git a/crates/kebab-parse-image/tests/extractor.rs b/crates/kebab-parse-image/tests/extractor.rs index 19889b0..459f26f 100644 --- a/crates/kebab-parse-image/tests/extractor.rs +++ b/crates/kebab-parse-image/tests/extractor.rs @@ -80,8 +80,8 @@ fn jpeg_with_exif_gps_captures_whitelisted_tags() { Some(&Value::String("2024-08-15T12:34:56".into())) ); assert_eq!(exif.get("orientation"), Some(&Value::Number(1.into()))); - let lat = exif.get("gps_lat").and_then(|v| v.as_f64()).expect("gps_lat"); - let lon = exif.get("gps_lon").and_then(|v| v.as_f64()).expect("gps_lon"); + let lat = exif.get("gps_lat").and_then(serde_json::Value::as_f64).expect("gps_lat"); + let lon = exif.get("gps_lon").and_then(serde_json::Value::as_f64).expect("gps_lon"); assert!((lat - 37.5).abs() < 1e-6, "lat={lat}"); assert!((lon - 127.0).abs() < 1e-6, "lon={lon}"); @@ -281,7 +281,7 @@ fn jpeg_with_gps_out_of_range_drops_latitude() { !exif.contains_key("gps_lat"), "out-of-range latitude must be dropped" ); - let lon = exif.get("gps_lon").and_then(|v| v.as_f64()).expect("gps_lon"); + let lon = exif.get("gps_lon").and_then(serde_json::Value::as_f64).expect("gps_lon"); assert!((lon - 127.0).abs() < 1e-6); } diff --git a/crates/kebab-parse-image/tests/ocr.rs b/crates/kebab-parse-image/tests/ocr.rs index 62fae92..a3f29f1 100644 --- a/crates/kebab-parse-image/tests/ocr.rs +++ b/crates/kebab-parse-image/tests/ocr.rs @@ -388,7 +388,7 @@ async fn ocr_integration_real_ollama_transcribes_text() { .expect("blocking task panicked") .expect("real Ollama OCR must succeed"); eprintln!("integration OCR result: {:?}", text.joined); - let normalized = text.joined.to_lowercase().replace(",", "").replace(".", ""); + let normalized = text.joined.to_lowercase().replace(',', "").replace('.', ""); assert!( normalized.contains("hello") && normalized.contains("world"), "integration OCR did not capture expected text: {:?}", diff --git a/crates/kebab-parse-md/Cargo.toml b/crates/kebab-parse-md/Cargo.toml index 912800a..7256b63 100644 --- a/crates/kebab-parse-md/Cargo.toml +++ b/crates/kebab-parse-md/Cargo.toml @@ -38,3 +38,6 @@ lingua = { version = "1.8", default-features = false, features = [ [dev-dependencies] serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-parse-md/src/blocks.rs b/crates/kebab-parse-md/src/blocks.rs index 19b5001..c3b277c 100644 --- a/crates/kebab-parse-md/src/blocks.rs +++ b/crates/kebab-parse-md/src/blocks.rs @@ -60,18 +60,15 @@ pub fn parse_blocks( let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { parse_blocks_inner(body, body_offset_lines) })); - match result { - Ok(out) => Ok(out), - Err(_) => { - tracing::warn!("parse_blocks panicked on adversarial input; returning empty"); - Ok(( - Vec::new(), - vec![Warning { - kind: WarningKind::ExtractFailed, - note: "pulldown-cmark panicked; body discarded".to_string(), - }], - )) - } + if let Ok(out) = result { Ok(out) } else { + tracing::warn!("parse_blocks panicked on adversarial input; returning empty"); + Ok(( + Vec::new(), + vec![Warning { + kind: WarningKind::ExtractFailed, + note: "pulldown-cmark panicked; body discarded".to_string(), + }], + )) } } @@ -102,9 +99,7 @@ fn parse_blocks_inner(body: &[u8], body_offset_lines: u32) -> (Vec, // possibly-inverted spans would be more harmful than dropping output. if state.overflow_detected { let at = state - .overflow_at_body_line - .map(|n| n.to_string()) - .unwrap_or_else(|| "?".to_string()); + .overflow_at_body_line.map_or_else(|| "?".to_string(), |n| n.to_string()); return ( Vec::new(), vec![Warning { @@ -339,10 +334,10 @@ impl InlineBuf { // `Inline::Link.text` field. Code/strong/emph inside a link are // collapsed to their plain text — `Inline::Link` doesn't model // formatting inside the link. - let flat = if !text.is_empty() { - text - } else { + let flat = if text.is_empty() { flatten_inlines_to_text(&kids) + } else { + text }; self.push_inline(Inline::Link { text: flat, href }); } @@ -364,10 +359,10 @@ impl InlineBuf { InlineFrame::Strong(kids) => self.push_inline(Inline::Strong { children: kids }), InlineFrame::Emph(kids) => self.push_inline(Inline::Emph { children: kids }), InlineFrame::Link { href, text, kids } => { - let flat = if !text.is_empty() { - text - } else { + let flat = if text.is_empty() { flatten_inlines_to_text(&kids) + } else { + text }; self.push_inline(Inline::Link { text: flat, href }); } @@ -528,20 +523,17 @@ impl<'a> WalkState<'a> { // inverted span. Without this guard, debug builds panic with // "attempt to add with overflow" (caught by `catch_unwind`, masking // the real cause) and release builds wrap to `start > end`. - match ( + if let (Some(start), Some(end)) = ( start_body.checked_add(self.body_offset_lines), end_body.checked_add(self.body_offset_lines), - ) { - (Some(start), Some(end)) => SourceSpan::Line { start, end }, - _ => { - if !self.overflow_detected { - self.overflow_detected = true; - self.overflow_at_body_line = Some(start_body); - } - SourceSpan::Line { - start: start_body.saturating_add(self.body_offset_lines), - end: end_body.saturating_add(self.body_offset_lines), - } + ) { SourceSpan::Line { start, end } } else { + if !self.overflow_detected { + self.overflow_detected = true; + self.overflow_at_body_line = Some(start_body); + } + SourceSpan::Line { + start: start_body.saturating_add(self.body_offset_lines), + end: end_body.saturating_add(self.body_offset_lines), } } } @@ -677,11 +669,11 @@ impl<'a> WalkState<'a> { } Event::Start(Tag::Strong) => { self.flag_non_image_in_paragraph(); - self.with_current_inlines(|buf| buf.open_strong()); + self.with_current_inlines(InlineBuf::open_strong); } Event::Start(Tag::Emphasis) => { self.flag_non_image_in_paragraph(); - self.with_current_inlines(|buf| buf.open_emph()); + self.with_current_inlines(InlineBuf::open_emph); } Event::Start(Tag::Link { dest_url, .. }) => { self.flag_non_image_in_paragraph(); @@ -991,13 +983,13 @@ impl<'a> WalkState<'a> { } } Event::End(TagEnd::Strong) => { - self.with_current_inlines(|buf| buf.close_strong()); + self.with_current_inlines(InlineBuf::close_strong); } Event::End(TagEnd::Emphasis) => { - self.with_current_inlines(|buf| buf.close_emph()); + self.with_current_inlines(InlineBuf::close_emph); } Event::End(TagEnd::Link) => { - self.with_current_inlines(|buf| buf.close_link()); + self.with_current_inlines(InlineBuf::close_link); } Event::End(TagEnd::Image) => { if let Some(Frame::Paragraph { image_depth, .. }) = self.frames.last_mut() { @@ -1480,8 +1472,7 @@ mod tests { inl, Inline::Text { .. } | Inline::Code { .. } | Inline::Link { .. } | Inline::Strong { .. } | Inline::Emph { .. } ), - "unexpected inline kind: {:?}", - inl + "unexpected inline kind: {inl:?}" ); } } @@ -1503,7 +1494,7 @@ mod tests { // First item should contain "a" plus a flattened rendering // of the nested sub-list. let flat = flatten_inlines_to_text(&items[0]); - assert!(flat.contains("a"), "first item missing 'a': {flat:?}"); + assert!(flat.contains('a'), "first item missing 'a': {flat:?}"); assert!(flat.contains("- x"), "first item missing '- x': {flat:?}"); assert!(flat.contains("- y"), "first item missing '- y': {flat:?}"); let flat2 = flatten_inlines_to_text(&items[1]); diff --git a/crates/kebab-parse-md/src/frontmatter.rs b/crates/kebab-parse-md/src/frontmatter.rs index 92c8a3c..6ff78bc 100644 --- a/crates/kebab-parse-md/src/frontmatter.rs +++ b/crates/kebab-parse-md/src/frontmatter.rs @@ -110,7 +110,7 @@ pub fn parse_frontmatter( } }; - let body_start = span_opt.map(|s| s.end).unwrap_or(0); + let body_start = span_opt.map_or(0, |s| s.end); let body = &bytes[body_start..]; let metadata = derive_metadata(raw_opt, hints, body, &mut warnings); @@ -430,30 +430,24 @@ fn derive_metadata( // ---- source_type ---- let source_type = match raw.source_type.as_deref() { None => SourceType::Markdown, - Some(s) => match parse_source_type(s) { - Some(st) => st, - None => { - warnings.push(Warning { - kind: WarningKind::MalformedFrontmatter, - note: format!("unknown source_type={s}, defaulted to markdown"), - }); - SourceType::Markdown - } + Some(s) => if let Some(st) = parse_source_type(s) { st } else { + warnings.push(Warning { + kind: WarningKind::MalformedFrontmatter, + note: format!("unknown source_type={s}, defaulted to markdown"), + }); + SourceType::Markdown }, }; // ---- trust_level ---- let trust_level = match raw.trust_level.as_deref() { None => TrustLevel::Primary, - Some(s) => match parse_trust_level(s) { - Some(tl) => tl, - None => { - warnings.push(Warning { - kind: WarningKind::MalformedFrontmatter, - note: format!("unknown trust_level={s}, defaulted to primary"), - }); - TrustLevel::Primary - } + Some(s) => if let Some(tl) = parse_trust_level(s) { tl } else { + warnings.push(Warning { + kind: WarningKind::MalformedFrontmatter, + note: format!("unknown trust_level={s}, defaulted to primary"), + }); + TrustLevel::Primary }, }; diff --git a/crates/kebab-parse-pdf/Cargo.toml b/crates/kebab-parse-pdf/Cargo.toml index d71532c..134c2c8 100644 --- a/crates/kebab-parse-pdf/Cargo.toml +++ b/crates/kebab-parse-pdf/Cargo.toml @@ -24,3 +24,6 @@ lopdf = "0.32" [dev-dependencies] blake3 = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-parse-pdf/src/lib.rs b/crates/kebab-parse-pdf/src/lib.rs index 5f1b90e..3d2505f 100644 --- a/crates/kebab-parse-pdf/src/lib.rs +++ b/crates/kebab-parse-pdf/src/lib.rs @@ -111,7 +111,7 @@ impl Extractor for PdfTextExtractor { }); let mut blocks: Vec = Vec::with_capacity(pages.len()); - for (&page_num, _) in pages.iter() { + for &page_num in pages.keys() { let (text, warning) = match page_text::extract_one(&pdf_doc, page_num) { Ok(t) if !t.trim().is_empty() => (t, None), Ok(_) => ( diff --git a/crates/kebab-parse-types/Cargo.toml b/crates/kebab-parse-types/Cargo.toml index 58bc504..4c7a42b 100644 --- a/crates/kebab-parse-types/Cargo.toml +++ b/crates/kebab-parse-types/Cargo.toml @@ -10,3 +10,6 @@ description = "Parser intermediate representations (no parser libs allowed)" [dependencies] kebab-core = { path = "../kebab-core" } serde = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-rag/Cargo.toml b/crates/kebab-rag/Cargo.toml index c0c3baf..5064fdf 100644 --- a/crates/kebab-rag/Cargo.toml +++ b/crates/kebab-rag/Cargo.toml @@ -28,3 +28,6 @@ kebab-llm = { path = "../kebab-llm", features = ["mock"] } tempfile = { workspace = true } rusqlite = { workspace = true } serde_json = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-rag/src/pipeline.rs b/crates/kebab-rag/src/pipeline.rs index 1a4b648..16c7663 100644 --- a/crates/kebab-rag/src/pipeline.rs +++ b/crates/kebab-rag/src/pipeline.rs @@ -318,7 +318,7 @@ impl RagPipeline { }); } let chunks_returned = u32::try_from(hits.len()).unwrap_or(u32::MAX); - let top_score = hits.first().map(|h| h.retrieval.fusion_score).unwrap_or(0.0); + let top_score = hits.first().map_or(0.0, |h| h.retrieval.fusion_score); tracing::debug!( target: "kebab-rag", @@ -856,7 +856,7 @@ impl RagPipeline { }); } let chunks_returned = u32::try_from(pool.len()).unwrap_or(u32::MAX); - let top_score = pool.first().map(|h| h.retrieval.fusion_score).unwrap_or(0.0); + let top_score = pool.first().map_or(0.0, |h| h.retrieval.fusion_score); // ── 3. Score gate / no chunks ────────────────────────────────────── // PR-3b-ii: forward the partial hop trace into the refusal so @@ -1149,7 +1149,7 @@ impl RagPipeline { refusal_phrase_detected = matched_refusal_phrase, finish_reason = ?finish_reason, chunks_used, - hops = answer.hops.as_ref().map(|v| v.len()).unwrap_or(0), + hops = answer.hops.as_ref().map_or(0, std::vec::Vec::len), "kb-rag: multi-hop ask done" ); @@ -1388,16 +1388,13 @@ impl RagPipeline { let chunk_full = ::get_chunk(&self.docs, &hit.chunk_id) .context("kb-rag: docs.get_chunk")?; - let chunk_text = match chunk_full { - Some(c) => c.text, - None => { - tracing::warn!( - target: "kebab-rag", - chunk_id = %hit.chunk_id.0, - "kb-rag: chunk not found in store; skipping" - ); - continue; - } + let chunk_text = if let Some(c) = chunk_full { c.text } else { + tracing::warn!( + target: "kebab-rag", + chunk_id = %hit.chunk_id.0, + "kb-rag: chunk not found in store; skipping" + ); + continue; }; let header = format!( "[#{n}] doc={} heading={} span={}\n", @@ -1999,13 +1996,11 @@ fn strip_markdown_json_fence(s: &str) -> &str { let after_open = trimmed .strip_prefix("```json") .or_else(|| trimmed.strip_prefix("```")) - .map(|rest| rest.trim_start_matches('\n')) - .unwrap_or(trimmed); + .map_or(trimmed, |rest| rest.trim_start_matches('\n')); let inner = after_open .trim_end() .strip_suffix("```") - .map(|rest| rest.trim_end()) - .unwrap_or(after_open); + .map_or(after_open, str::trim_end); inner.trim() } diff --git a/crates/kebab-rag/tests/common/mod.rs b/crates/kebab-rag/tests/common/mod.rs index 6d64c0b..6d30b2c 100644 --- a/crates/kebab-rag/tests/common/mod.rs +++ b/crates/kebab-rag/tests/common/mod.rs @@ -147,7 +147,7 @@ pub fn mk_hit_with_indexed_at( chunk_id: ChunkId(chunk_id.to_string()), doc_id: DocumentId(doc_id.to_string()), doc_path: p.clone(), - heading_path: heading.iter().map(|s| s.to_string()).collect(), + heading_path: heading.iter().map(std::string::ToString::to_string).collect(), section_label: None, snippet: "snippet".to_string(), citation: Citation::Line { diff --git a/crates/kebab-rag/tests/multi_hop.rs b/crates/kebab-rag/tests/multi_hop.rs index ae21e64..13f3bbd 100644 --- a/crates/kebab-rag/tests/multi_hop.rs +++ b/crates/kebab-rag/tests/multi_hop.rs @@ -68,7 +68,7 @@ fn multi_hop_decide_stop_triggers_synthesize() { // Three LLM calls in order: decompose → decide → synthesize. let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1"]"#, - r#"[]"#, + r"[]", "answer body [#1]", ])); let lm_handle = lm.clone(); @@ -131,7 +131,7 @@ fn multi_hop_decide_continue_adds_more_chunks() { let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1"]"#, r#"["q2"]"#, - r#"[]"#, + r"[]", "synthesized [#1] [#2]", ])); let lm_handle = lm.clone(); @@ -255,7 +255,7 @@ fn multi_hop_pool_chunks_dedup_by_chunk_id() { let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1", "q2"]"#, - r#"[]"#, + r"[]", "merged answer [#1]", ])); let lm_handle = lm.clone(); @@ -444,7 +444,7 @@ fn multi_hop_refuse_score_gate_preserves_hops_trace() { // never runs because we refuse before pack_context. let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1"]"#, - r#"[]"#, + r"[]", ])); let lm_handle = lm.clone(); let lm_dyn: Arc = lm; @@ -594,7 +594,7 @@ fn multi_hop_above_probe_gate_proceeds_to_decompose() { let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1"]"#, - r#"[]"#, + r"[]", "answer [#1]", ])); let lm_handle = lm.clone(); @@ -649,7 +649,7 @@ fn happy_multi_hop_env() -> (RagEnv, Arc, Arc) { let retriever = Arc::new(ScriptedRetriever::new(vec![hits.clone(), hits])); let lm = Arc::new(ScriptedLm::new(vec![ r#"["q1"]"#, - r#"[]"#, + r"[]", "answer body [#1]", ])); (env, retriever, lm) diff --git a/crates/kebab-rag/tests/pipeline.rs b/crates/kebab-rag/tests/pipeline.rs index 148c0f6..ae0016c 100644 --- a/crates/kebab-rag/tests/pipeline.rs +++ b/crates/kebab-rag/tests/pipeline.rs @@ -282,8 +282,7 @@ fn streaming_forwards_tokens_to_sink() { StreamEvent::Token { delta, .. } => Some(delta), _ => None, }) - .collect::>() - .join(""); + .collect::(); assert_eq!(collected, canned); } @@ -522,7 +521,7 @@ fn answer_json_serializes_with_expected_keys() { let answer = pipeline.ask("what", default_opts()).unwrap(); let v: serde_json::Value = serde_json::to_value(&answer).unwrap(); // Stable top-level key set per `answer.v1` (§2.3). - let keys: Vec<&str> = v.as_object().unwrap().keys().map(|s| s.as_str()).collect(); + let keys: Vec<&str> = v.as_object().unwrap().keys().map(std::string::String::as_str).collect(); for needed in [ "answer", "citations", diff --git a/crates/kebab-search/Cargo.toml b/crates/kebab-search/Cargo.toml index f13fd7f..adc8e0e 100644 --- a/crates/kebab-search/Cargo.toml +++ b/crates/kebab-search/Cargo.toml @@ -36,3 +36,6 @@ tempfile = { workspace = true } # The mock-retriever unit tests (the bulk of the hybrid suite) do not # need either, but the integration / snapshot lane does. kebab-embed = { path = "../kebab-embed", features = ["mock"] } + +[lints] +workspace = true diff --git a/crates/kebab-search/src/hybrid.rs b/crates/kebab-search/src/hybrid.rs index 3378f51..306afb0 100644 --- a/crates/kebab-search/src/hybrid.rs +++ b/crates/kebab-search/src/hybrid.rs @@ -601,7 +601,7 @@ mod tests { let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 5); let out = h.search(&make_query(SearchMode::Hybrid, 5)).unwrap(); let a = out.iter().find(|h| h.chunk_id.0 == "aaaa").unwrap(); - let actual = a.retrieval.fusion_score as f64; + let actual = f64::from(a.retrieval.fusion_score); // Tolerance: the score is computed in f64 and cast to f32 at // the API boundary, so any discrepancy must fit within f32 // precision. `1e-7` is below `f32::EPSILON` (~1.19e-7), which @@ -694,7 +694,7 @@ mod tests { let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 4); let out = h.search(&make_query(SearchMode::Hybrid, 4)).unwrap(); let mut ids: Vec<&str> = out.iter().map(|h| h.chunk_id.0.as_str()).collect(); - ids.sort(); + ids.sort_unstable(); assert_eq!(ids, vec!["aaaa", "bbbb", "cccc", "dddd"]); } diff --git a/crates/kebab-search/src/lexical.rs b/crates/kebab-search/src/lexical.rs index 09351e8..678c2d1 100644 --- a/crates/kebab-search/src/lexical.rs +++ b/crates/kebab-search/src/lexical.rs @@ -457,7 +457,7 @@ fn run_query( .prepare(&sql) .context("kb-search lexical: prepare FTS5 statement")?; let rows = stmt - .query_map(params_from_iter(params.iter().map(|b| b.as_ref())), row_from_sql) + .query_map(params_from_iter(params.iter().map(std::convert::AsRef::as_ref)), row_from_sql) .context("kb-search lexical: execute FTS5 query")?; let mut out: Vec = Vec::new(); for r in rows { diff --git a/crates/kebab-search/tests/common/mod.rs b/crates/kebab-search/tests/common/mod.rs index d0ae1ad..354f141 100644 --- a/crates/kebab-search/tests/common/mod.rs +++ b/crates/kebab-search/tests/common/mod.rs @@ -37,12 +37,10 @@ use tempfile::TempDir; pub fn require_avx_or_panic() { #[cfg(target_arch = "x86_64")] { - if !std::is_x86_feature_detected!("avx") { - panic!( - "kb-search hybrid integration test requires AVX-capable hardware; \ - host CPU lacks AVX. Run on an AVX-capable machine." - ); - } + assert!(std::is_x86_feature_detected!("avx"), + "kb-search hybrid integration test requires AVX-capable hardware; \ + host CPU lacks AVX. Run on an AVX-capable machine." + ); } } @@ -285,7 +283,7 @@ impl HybridEnv { vector, doc_id: DocumentId(doc_id.to_string()), text: text.to_string(), - heading_path: heading_path.iter().map(|s| s.to_string()).collect(), + heading_path: heading_path.iter().map(std::string::ToString::to_string).collect(), model_id: EmbeddingModelId(TEST_MODEL_ID.to_string()), model_version: EmbeddingVersion("v1".to_string()), dimensions: TEST_DIMENSIONS, diff --git a/crates/kebab-search/tests/hybrid.rs b/crates/kebab-search/tests/hybrid.rs index 912422a..c60ce97 100644 --- a/crates/kebab-search/tests/hybrid.rs +++ b/crates/kebab-search/tests/hybrid.rs @@ -186,14 +186,12 @@ fn hybrid_snapshot_run_1() { // Refuse to silently "pass" against the committed placeholder. The // placeholder JSON carries a `_comment` field with regeneration // instructions; production fixtures (a captured list) do not. - if expected.get("_comment").is_some() { - panic!( - "snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \ - Path: {}. To regenerate: \ - `KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.", - fixture.display() - ); - } + assert!(!expected.get("_comment").is_some(), + "snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \ + Path: {}. To regenerate: \ + `KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.", + fixture.display() + ); assert_eq!( actual, expected, diff --git a/crates/kebab-source-fs/Cargo.toml b/crates/kebab-source-fs/Cargo.toml index 68ea7be..b7236f3 100644 --- a/crates/kebab-source-fs/Cargo.toml +++ b/crates/kebab-source-fs/Cargo.toml @@ -23,3 +23,6 @@ globset = "0.4" [dev-dependencies] serde_json = { workspace = true } tempfile = "3" + +[lints] +workspace = true diff --git a/crates/kebab-source-fs/src/media.rs b/crates/kebab-source-fs/src/media.rs index 8dfc2df..0299b72 100644 --- a/crates/kebab-source-fs/src/media.rs +++ b/crates/kebab-source-fs/src/media.rs @@ -21,7 +21,7 @@ pub(crate) fn media_type_for(path: &Path) -> MediaType { let ext = path .extension() .and_then(|s| s.to_str()) - .map(|s| s.to_ascii_lowercase()) + .map(str::to_ascii_lowercase) .unwrap_or_default(); match ext.as_str() { diff --git a/crates/kebab-source-fs/src/walker.rs b/crates/kebab-source-fs/src/walker.rs index 9f6a540..cfc8252 100644 --- a/crates/kebab-source-fs/src/walker.rs +++ b/crates/kebab-source-fs/src/walker.rs @@ -183,7 +183,7 @@ pub(crate) fn build_overrides( // Per-source matchers (for attribution only). let gitignore = - build_single_matcher(root, &gitignore_patterns.iter().map(|s| s.as_str()).collect::>())?; + build_single_matcher(root, &gitignore_patterns.iter().map(std::string::String::as_str).collect::>())?; let kebabignore = build_single_matcher_owned(root, kbignore_patterns)?; // Use the directory-aware builtin matcher so that `is_dir=true` checks on // directory entries (e.g., `node_modules/`) are attributed to builtin rather @@ -328,9 +328,9 @@ pub(crate) fn read_kbignore(root: &Path) -> Result> { .with_context(|| format!("failed to read {}", path.display()))?; Ok(text .lines() - .map(|l| l.trim()) + .map(str::trim) .filter(|l| !l.is_empty() && !l.starts_with('#')) - .map(|l| l.to_string()) + .map(std::string::ToString::to_string) .collect()) } @@ -708,8 +708,7 @@ mod tests { .collect(); assert!( gitignore_skipped.iter().any(|e| e.path.file_name() - .map(|n| n == "skipme.log") - .unwrap_or(false)), + .is_some_and(|n| n == "skipme.log")), "skipme.log should appear in gitignore_skipped; skipped: {:?}", skipped_entries.iter().map(|e| &e.path).collect::>() ); diff --git a/crates/kebab-store-sqlite/Cargo.toml b/crates/kebab-store-sqlite/Cargo.toml index 2cc17ed..7322b61 100644 --- a/crates/kebab-store-sqlite/Cargo.toml +++ b/crates/kebab-store-sqlite/Cargo.toml @@ -37,3 +37,6 @@ serde_json = { workspace = true } kebab-parse-md = { path = "../kebab-parse-md" } kebab-normalize = { path = "../kebab-normalize" } kebab-chunk = { path = "../kebab-chunk" } + +[lints] +workspace = true diff --git a/crates/kebab-store-sqlite/src/answers.rs b/crates/kebab-store-sqlite/src/answers.rs index 9d3eec8..3e870bf 100644 --- a/crates/kebab-store-sqlite/src/answers.rs +++ b/crates/kebab-store-sqlite/src/answers.rs @@ -63,7 +63,7 @@ impl SqliteStore { answer.retrieval.trace_id.0, query, answer.answer, - if answer.grounded { 1_i64 } else { 0_i64 }, + i64::from(answer.grounded), refusal_label, answer.model.id, answer.model.provider, @@ -72,15 +72,15 @@ impl SqliteStore { answer.prompt_template_version.0, mode_label, answer.retrieval.k as i64, - answer.retrieval.score_gate as f64, - answer.retrieval.top_score as f64, - answer.retrieval.chunks_returned as i64, - answer.retrieval.chunks_used as i64, + f64::from(answer.retrieval.score_gate), + f64::from(answer.retrieval.top_score), + i64::from(answer.retrieval.chunks_returned), + i64::from(answer.retrieval.chunks_used), citations_json, packed_chunks_json, - answer.usage.prompt_tokens as i64, - answer.usage.completion_tokens as i64, - answer.usage.latency_ms as i64, + i64::from(answer.usage.prompt_tokens), + i64::from(answer.usage.completion_tokens), + i64::from(answer.usage.latency_ms), created_at, ], ) diff --git a/crates/kebab-store-sqlite/src/documents.rs b/crates/kebab-store-sqlite/src/documents.rs index 277eb9a..02b9b26 100644 --- a/crates/kebab-store-sqlite/src/documents.rs +++ b/crates/kebab-store-sqlite/src/documents.rs @@ -270,12 +270,12 @@ impl kebab_core::DocumentStore for SqliteStore { ) -> Result> { let conn = self.lock_conn(); let result = conn.query_row( - r#"SELECT + r"SELECT asset_id, source_uri, workspace_path, media_type, byte_len, checksum, storage_kind, storage_path, discovered_at FROM assets - WHERE asset_id = ?"#, + WHERE asset_id = ?", rusqlite::params![id.0.as_str()], asset_from_row, ); @@ -292,12 +292,12 @@ impl kebab_core::DocumentStore for SqliteStore { ) -> Result> { let conn = self.lock_conn(); let result = conn.query_row( - r#"SELECT + r"SELECT asset_id, source_uri, workspace_path, media_type, byte_len, checksum, storage_kind, storage_path, discovered_at FROM assets - WHERE workspace_path = ?"#, + WHERE workspace_path = ?", rusqlite::params![path.0.as_str()], asset_from_row, ); @@ -456,7 +456,7 @@ impl kebab_core::DocumentStore for SqliteStore { let mut stmt = conn.prepare(&sql).map_err(StoreError::from)?; let rows = stmt .query_map( - rusqlite::params_from_iter(params_dyn.iter().map(|b| b.as_ref())), + rusqlite::params_from_iter(params_dyn.iter().map(std::convert::AsRef::as_ref)), doc_summary_from_sql, ) .map_err(StoreError::from)?; @@ -774,8 +774,8 @@ fn upsert_document( source_type, trust_level, doc.parser_version.0, - doc.doc_version as i64, - doc.schema_version as i64, + i64::from(doc.doc_version), + i64::from(doc.schema_version), metadata_json, provenance_json, created_at, diff --git a/crates/kebab-store-sqlite/src/filters.rs b/crates/kebab-store-sqlite/src/filters.rs index 9c68829..841f42a 100644 --- a/crates/kebab-store-sqlite/src/filters.rs +++ b/crates/kebab-store-sqlite/src/filters.rs @@ -224,7 +224,7 @@ impl SqliteStore { .context("kb-store-sqlite::filter_chunks: prepare SQL")?; let rows = stmt .query_map( - params_from_iter(bind.iter().map(|b| b.as_ref())), + params_from_iter(bind.iter().map(std::convert::AsRef::as_ref)), |row| { let chunk_id: String = row.get(0)?; let workspace_path: String = row.get(1)?; @@ -594,7 +594,7 @@ mod tests { ) .unwrap(); let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect(); - got.sort(); + got.sort_unstable(); assert_eq!(got, vec![chunks[0].0, chunks[2].0, chunks[3].0]); // + lang=en → drops c3. @@ -610,7 +610,7 @@ mod tests { ) .unwrap(); let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect(); - got.sort(); + got.sort_unstable(); assert_eq!(got, vec![chunks[0].0, chunks[3].0]); // + trust_min=Secondary → drops c4 (generated < secondary). @@ -641,7 +641,7 @@ mod tests { ) .unwrap(); let mut got: Vec<&str> = out.iter().map(|c| c.0.as_str()).collect(); - got.sort(); + got.sort_unstable(); assert_eq!(got, vec![chunks[0].0, chunks[1].0, chunks[2].0]); } @@ -795,7 +795,7 @@ mod tests { seed_committed_with_metadata( &store, c3, "d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3", "README.md", r#""markdown""#, - r#"{}"#, + r"{}", ); let f = SearchFilters { diff --git a/crates/kebab-store-sqlite/src/jobs.rs b/crates/kebab-store-sqlite/src/jobs.rs index dbeb4be..c9136ba 100644 --- a/crates/kebab-store-sqlite/src/jobs.rs +++ b/crates/kebab-store-sqlite/src/jobs.rs @@ -69,12 +69,12 @@ impl SqliteStore { params![ row.run_id, row.scope_json, - row.scanned as i64, - row.new_count as i64, - row.updated_count as i64, - row.skipped_count as i64, - row.error_count as i64, - row.duration_ms as i64, + i64::from(row.scanned), + i64::from(row.new_count), + i64::from(row.updated_count), + i64::from(row.skipped_count), + i64::from(row.error_count), + i64::from(row.duration_ms), started, finished, row.items_json, @@ -191,7 +191,7 @@ impl kebab_core::JobRepo for SqliteStore { let mut stmt = conn.prepare(&sql).map_err(StoreError::from)?; let rows = stmt .query_map( - rusqlite::params_from_iter(params_dyn.iter().map(|b| b.as_ref())), + rusqlite::params_from_iter(params_dyn.iter().map(std::convert::AsRef::as_ref)), job_row_from_sql, ) .map_err(StoreError::from)?; diff --git a/crates/kebab-store-sqlite/src/store.rs b/crates/kebab-store-sqlite/src/store.rs index b1f54e1..23df426 100644 --- a/crates/kebab-store-sqlite/src/store.rs +++ b/crates/kebab-store-sqlite/src/store.rs @@ -163,7 +163,7 @@ impl SqliteStore { /// safe to reuse — we simply unwrap the inner guard rather than /// propagate the panic to every subsequent call. pub(crate) fn lock_conn(&self) -> MutexGuard<'_, Connection> { - self.conn.lock().unwrap_or_else(|p| p.into_inner()) + self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner) } /// Read-only borrow of the connection. @@ -179,7 +179,7 @@ impl SqliteStore { /// /// Poisoning is recovered the same way as [`Self::lock_conn`]. pub fn read_conn(&self) -> MutexGuard<'_, Connection> { - self.conn.lock().unwrap_or_else(|p| p.into_inner()) + self.conn.lock().unwrap_or_else(std::sync::PoisonError::into_inner) } /// Persist a `RawAsset` *with its raw bytes*: row goes into `assets`, @@ -359,9 +359,7 @@ fn temp_path_for(dest: &Path) -> PathBuf { let n = TEMP_SUFFIX_COUNTER.fetch_add(1, Ordering::Relaxed); let parent = dest.parent().unwrap_or_else(|| Path::new(".")); let file_name = dest - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| "asset".to_string()); + .file_name().map_or_else(|| "asset".to_string(), |s| s.to_string_lossy().into_owned()); parent.join(format!("{file_name}.tmp.{pid}.{n}")) } diff --git a/crates/kebab-store-sqlite/tests/fts.rs b/crates/kebab-store-sqlite/tests/fts.rs index 7f6d346..c0dc660 100644 --- a/crates/kebab-store-sqlite/tests/fts.rs +++ b/crates/kebab-store-sqlite/tests/fts.rs @@ -86,7 +86,7 @@ fn fts_v002_backfills_existing_chunks() { // on a customer DB upgrading from P1 to P2-1). const N: usize = 4; for i in 0..N { - let cid = format!("{:0>32}", i); + let cid = format!("{i:0>32}"); insert_chunk( &conn, &cid, @@ -112,7 +112,7 @@ fn fts_v002_backfills_existing_chunks() { "V002 backfill INSERT must seed one chunks_fts row per chunks row" ); for i in 0..N { - let cid = format!("{:0>32}", i); + let cid = format!("{i:0>32}"); let term = format!("seedrow{i}"); let hit: String = conn .query_row( @@ -137,7 +137,7 @@ fn fts_v002_backfill_select_matches_chunks_count() { let conn = raw_conn_no_fk(&env); for i in 0..5 { - let cid = format!("{:0>32}", i); + let cid = format!("{i:0>32}"); insert_chunk(&conn, &cid, &"d".repeat(32), "[]", &format!("row {i}")); } // Wipe + run the literal V002 backfill INSERT. @@ -250,7 +250,7 @@ fn fts_rebuild_chunks_fts_is_idempotent() { let conn = raw_conn_no_fk(&env); for i in 0..3 { - let cid = format!("{:0>32}", i); + let cid = format!("{i:0>32}"); insert_chunk(&conn, &cid, &"d".repeat(32), "[]", &format!("token{i}")); } let before = count(&conn, "chunks_fts"); @@ -396,8 +396,7 @@ fn extract_migration_5_5_verbatim_block() -> String { // Walk back from the close marker to the start of its comment line. let close_line_start = migration[..close_idx] .rfind('\n') - .map(|n| n + 1) - .unwrap_or(0); + .map_or(0, |n| n + 1); migration[after_open_line..close_line_start].to_string() } diff --git a/crates/kebab-store-vector/Cargo.toml b/crates/kebab-store-vector/Cargo.toml index d40ac0e..a379bda 100644 --- a/crates/kebab-store-vector/Cargo.toml +++ b/crates/kebab-store-vector/Cargo.toml @@ -53,3 +53,6 @@ serde_json = { workspace = true } # touch rusqlite directly (P3-3 spec: kb-store-vector must not list # rusqlite/globset as direct deps). rusqlite = { workspace = true } + +[lints] +workspace = true diff --git a/crates/kebab-store-vector/src/arrow_batch.rs b/crates/kebab-store-vector/src/arrow_batch.rs index 1e1910e..3182b43 100644 --- a/crates/kebab-store-vector/src/arrow_batch.rs +++ b/crates/kebab-store-vector/src/arrow_batch.rs @@ -189,8 +189,8 @@ mod tests { fn make_rec(chunk_idx: u8, dim: usize) -> VectorRecord { VectorRecord { - chunk_id: ChunkId(format!("{:032x}", chunk_idx)), - embedding_id: EmbeddingId(format!("{:032x}", 0xeeeeu16 + chunk_idx as u16)), + chunk_id: ChunkId(format!("{chunk_idx:032x}")), + embedding_id: EmbeddingId(format!("{:032x}", 0xeeeeu16 + u16::from(chunk_idx))), vector: vec![0.1_f32; dim], doc_id: DocumentId("aaaa".repeat(8)), text: format!("text-{chunk_idx}"), diff --git a/crates/kebab-store-vector/src/store.rs b/crates/kebab-store-vector/src/store.rs index bfa922d..007670f 100644 --- a/crates/kebab-store-vector/src/store.rs +++ b/crates/kebab-store-vector/src/store.rs @@ -169,16 +169,13 @@ impl LanceVectorStore { arrow_schema::DataType::FixedSizeList(_, table_dim) => { if (*table_dim as usize) != dim { anyhow::bail!( - "dimension mismatch: table has dim {}, records have dim {}", - table_dim, - dim + "dimension mismatch: table has dim {table_dim}, records have dim {dim}" ); } Ok(()) } other => anyhow::bail!( - "embedding column has unexpected Arrow type {:?}", - other + "embedding column has unexpected Arrow type {other:?}" ), } } @@ -390,19 +387,15 @@ impl VectorStore for LanceVectorStore { // matching `query_vec.len()`. In v1 there's typically one // model in play; if there are several we pick the first match. let dim = query_vec.len(); - let table_name = match self + let table_name = if let Some(name) = self .runtime - .block_on(async { find_matching_table(&self.connection, dim).await })? - { - Some(name) => name, - None => { - tracing::debug!( - target: "kebab-store-vector", - dim, - "search: no Lance table matches query dim — returning empty" - ); - return Ok(Vec::new()); - } + .block_on(async { find_matching_table(&self.connection, dim).await })? { name } else { + tracing::debug!( + target: "kebab-store-vector", + dim, + "search: no Lance table matches query dim — returning empty" + ); + return Ok(Vec::new()); }; // Pre-fetch 2*k Lance rows; we'll filter against SQLite @@ -574,7 +567,7 @@ fn score_from_distance(distance: f32) -> f32 { return 0.0; } let sim = 1.0 - distance; - (sim + 1.0) / 2.0 + f32::midpoint(sim, 1.0) } /// Find a Lance table whose embedding column is FixedSizeList. diff --git a/crates/kebab-store-vector/tests/common/mod.rs b/crates/kebab-store-vector/tests/common/mod.rs index a05b68c..7bb5a2a 100644 --- a/crates/kebab-store-vector/tests/common/mod.rs +++ b/crates/kebab-store-vector/tests/common/mod.rs @@ -49,13 +49,11 @@ use std::sync::Arc; pub fn require_avx_or_panic() { #[cfg(target_arch = "x86_64")] { - if !std::is_x86_feature_detected!("avx") { - panic!( - "kb-store-vector integration test requires AVX-capable hardware; \ - host CPU lacks AVX. Run on an AVX-capable machine. \ - See crates/kb-store-vector/tests/common/mod.rs." - ); - } + assert!(std::is_x86_feature_detected!("avx"), + "kb-store-vector integration test requires AVX-capable hardware; \ + host CPU lacks AVX. Run on an AVX-capable machine. \ + See crates/kb-store-vector/tests/common/mod.rs." + ); } } @@ -167,17 +165,17 @@ pub fn make_record( model: &str, ) -> VectorRecord { let dim = vector.len(); - let chunk_id = ChunkId(format!("{:032x}", 0x1100u32 + chunk_idx as u32)); - let doc_id = DocumentId(format!("{:032x}", 0xd0c0u32 + doc_idx as u32)); + let chunk_id = ChunkId(format!("{:032x}", 0x1100u32 + u32::from(chunk_idx))); + let doc_id = DocumentId(format!("{:032x}", 0xd0c0u32 + u32::from(doc_idx))); let embedding_id = - EmbeddingId(format!("{:032x}", 0xeeee0000u32 + chunk_idx as u32)); + EmbeddingId(format!("{:032x}", 0xeeee0000u32 + u32::from(chunk_idx))); VectorRecord { chunk_id, embedding_id, vector, doc_id, text: text.to_string(), - heading_path: heading.iter().map(|s| s.to_string()).collect(), + heading_path: heading.iter().map(std::string::ToString::to_string).collect(), model_id: EmbeddingModelId(model.to_string()), model_version: EmbeddingVersion("v1".to_string()), dimensions: dim, diff --git a/crates/kebab-store-vector/tests/snapshot.rs b/crates/kebab-store-vector/tests/snapshot.rs index 8f33702..05c63da 100644 --- a/crates/kebab-store-vector/tests/snapshot.rs +++ b/crates/kebab-store-vector/tests/snapshot.rs @@ -93,14 +93,12 @@ fn vector_hits_snapshot_run_1() { // placeholder. The placeholder JSON carries a `_comment` field // with regeneration instructions; production fixtures (a captured // hits array) do not. - if expected.get("_comment").is_some() { - panic!( - "snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \ - Path: {}. To regenerate: \ - `KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-store-vector -- --ignored snapshot`.", - fixture.display() - ); - } + assert!(!expected.get("_comment").is_some(), + "snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \ + Path: {}. To regenerate: \ + `KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-store-vector -- --ignored snapshot`.", + fixture.display() + ); assert_eq!( actual, expected, diff --git a/crates/kebab-store-vector/tests/upsert_search.rs b/crates/kebab-store-vector/tests/upsert_search.rs index 2dab53b..cdd83f5 100644 --- a/crates/kebab-store-vector/tests/upsert_search.rs +++ b/crates/kebab-store-vector/tests/upsert_search.rs @@ -67,7 +67,7 @@ fn upsert_ten_then_search_returns_five() { // for the rest, with small per-row jitter so they stay // distinct in the index. let mut v = if i < 5 { dir(0) } else { dir(1) }; - v[3] = (i as f32) * 0.001; + v[3] = f32::from(i) * 0.001; let rec = make_record(i, i, v, &format!("text-{i}"), &["A"], MODEL); env.seed_chunk( &rec.chunk_id.0, @@ -264,7 +264,7 @@ fn determinism_same_query_same_top_k() { let recs: Vec<_> = (0..6u8) .map(|i| { let mut v = dir(i % 4); - v[3] = (i as f32) * 0.001; + v[3] = f32::from(i) * 0.001; let rec = make_record(i, i, v, &format!("t-{i}"), &[], MODEL); env.seed_chunk( &rec.chunk_id.0, diff --git a/crates/kebab-tui/Cargo.toml b/crates/kebab-tui/Cargo.toml index d55efbe..72ce1e4 100644 --- a/crates/kebab-tui/Cargo.toml +++ b/crates/kebab-tui/Cargo.toml @@ -36,3 +36,6 @@ pulldown-cmark = { version = "0.13", default-features = false } [dev-dependencies] tempfile = { workspace = true } kebab-app = { path = "../kebab-app" } + +[lints] +workspace = true diff --git a/crates/kebab-tui/src/ask.rs b/crates/kebab-tui/src/ask.rs index 6a6dccf..836cb4b 100644 --- a/crates/kebab-tui/src/ask.rs +++ b/crates/kebab-tui/src/ask.rs @@ -100,7 +100,7 @@ fn render_answer(f: &mut Frame, area: Rect, s: &AskState, theme: &crate::theme:: let title = if s.turns.is_empty() && !s.streaming { "transcript".to_string() } else { - let count = s.turns.len() + if s.streaming { 1 } else { 0 }; + let count = s.turns.len() + usize::from(s.streaming); format!("transcript ({} turn{})", count, if count == 1 { "" } else { "s" }) }; let block = Block::default().title(title).borders(Borders::ALL); @@ -409,8 +409,7 @@ pub fn handle_key_ask(state: &mut App, key: KeyEvent) -> KeyOutcome { if state .ask .as_ref() - .map(|s| s.streaming || s.thread.is_some() || s.input.as_str().trim().is_empty()) - .unwrap_or(true) + .is_none_or(|s| s.streaming || s.thread.is_some() || s.input.as_str().trim().is_empty()) { return KeyOutcome::Continue; } @@ -588,7 +587,7 @@ fn make_conversation_id() -> String { .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_nanos()) .unwrap_or(0); - format!("conv_{:032x}", nanos) + format!("conv_{nanos:032x}") } /// Run-loop hook: drain the streaming channel into `partial`. Called @@ -619,8 +618,7 @@ pub(crate) fn poll_worker(state: &mut App) { let finished = s .thread .as_ref() - .map(|h| h.is_finished()) - .unwrap_or(false); + .is_some_and(std::thread::JoinHandle::is_finished); if !finished { return; } diff --git a/crates/kebab-tui/src/cheatsheet.rs b/crates/kebab-tui/src/cheatsheet.rs index 49237ce..4a69f6b 100644 --- a/crates/kebab-tui/src/cheatsheet.rs +++ b/crates/kebab-tui/src/cheatsheet.rs @@ -140,7 +140,7 @@ fn push_section( for (key, desc) in keys { lines.push(Line::from(vec![ Span::raw(" "), - Span::styled(format!("{:<18}", key), theme.style(Role::CitationMarker)), + Span::styled(format!("{key:<18}"), theme.style(Role::CitationMarker)), Span::raw(" "), Span::raw(desc.to_string()), ])); diff --git a/crates/kebab-tui/src/editor.rs b/crates/kebab-tui/src/editor.rs index b60fa63..57feaab 100644 --- a/crates/kebab-tui/src/editor.rs +++ b/crates/kebab-tui/src/editor.rs @@ -54,7 +54,7 @@ pub(crate) fn with_external_program( struct Restore<'a> { terminal: &'a mut TuiTerminal, } - impl<'a> Drop for Restore<'a> { + impl Drop for Restore<'_> { fn drop(&mut self) { // Best-effort: errors here would clobber an in-flight // panic if propagated. Match the conservative posture in diff --git a/crates/kebab-tui/src/error_popup.rs b/crates/kebab-tui/src/error_popup.rs index 67c9125..0e20d94 100644 --- a/crates/kebab-tui/src/error_popup.rs +++ b/crates/kebab-tui/src/error_popup.rs @@ -23,7 +23,7 @@ pub struct ErrorOverlay { impl ErrorOverlay { pub fn from_anyhow(err: &anyhow::Error) -> Self { - let chain: Vec = err.chain().map(|c| c.to_string()).collect(); + let chain: Vec = err.chain().map(std::string::ToString::to_string).collect(); Self { title: "error".to_string(), chain, diff --git a/crates/kebab-tui/src/ingest_progress.rs b/crates/kebab-tui/src/ingest_progress.rs index d6c6392..62eab84 100644 --- a/crates/kebab-tui/src/ingest_progress.rs +++ b/crates/kebab-tui/src/ingest_progress.rs @@ -204,9 +204,9 @@ pub fn status_line(state: &IngestState) -> String { } if state.counts.scanned == 0 { let secs = state.started_at.elapsed().as_secs(); - return format!("ingest: scanning… [{}s]", secs); + return format!("ingest: scanning… [{secs}s]"); } - let pct = (state.current_idx as u64).saturating_mul(100) / state.counts.scanned.max(1) as u64; + let pct = u64::from(state.current_idx).saturating_mul(100) / u64::from(state.counts.scanned.max(1)); let elapsed = state.started_at.elapsed(); let mm = elapsed.as_secs() / 60; let ss = elapsed.as_secs() % 60; diff --git a/crates/kebab-tui/src/inspect.rs b/crates/kebab-tui/src/inspect.rs index 2910b1b..3b88214 100644 --- a/crates/kebab-tui/src/inspect.rs +++ b/crates/kebab-tui/src/inspect.rs @@ -53,10 +53,10 @@ pub fn render_inspect(f: &mut Frame, area: Rect, state: &App) { let threshold_days = state.config.search.stale_threshold_days; match (&s.target, &s.doc, &s.chunk) { (Some(InspectTarget::Doc(_)), Some(doc), _) => { - render_doc(f, area, s, doc, &state.theme, threshold_days) + render_doc(f, area, s, doc, &state.theme, threshold_days); } (Some(InspectTarget::Chunk(_)), _, Some(chunk)) => { - render_chunk(f, area, s, chunk, &state.theme) + render_chunk(f, area, s, chunk, &state.theme); } _ => { let block = RBlock::default() @@ -477,12 +477,12 @@ pub fn handle_key_inspect(state: &mut App, key: KeyEvent) -> KeyOutcome { return KeyOutcome::SwitchPane(Pane::Library); }; match (key.code, key.modifiers) { - (KeyCode::Esc, _) | (KeyCode::Char('q'), _) => KeyOutcome::SwitchPane(s.return_to), - (KeyCode::Char('j'), _) | (KeyCode::Down, _) => { + (KeyCode::Esc | KeyCode::Char('q'), _) => KeyOutcome::SwitchPane(s.return_to), + (KeyCode::Char('j') | KeyCode::Down, _) => { s.scroll = s.scroll.saturating_add(1); KeyOutcome::Continue } - (KeyCode::Char('k'), _) | (KeyCode::Up, _) => { + (KeyCode::Char('k') | KeyCode::Up, _) => { s.scroll = s.scroll.saturating_sub(1); KeyOutcome::Continue } diff --git a/crates/kebab-tui/src/library.rs b/crates/kebab-tui/src/library.rs index 4332d04..2bf2e30 100644 --- a/crates/kebab-tui/src/library.rs +++ b/crates/kebab-tui/src/library.rs @@ -328,15 +328,15 @@ pub fn handle_key_library(state: &mut App, key: KeyEvent) -> KeyOutcome { let pending_g = std::mem::take(&mut inner.pending_g); match (key.code, key.modifiers) { - (KeyCode::Char('q'), _) | (KeyCode::Esc, _) => { + (KeyCode::Char('q') | KeyCode::Esc, _) => { state.should_quit = true; KeyOutcome::Quit } - (KeyCode::Char('j'), _) | (KeyCode::Down, _) => { + (KeyCode::Char('j') | KeyCode::Down, _) => { move_selection(inner, 1); KeyOutcome::Continue } - (KeyCode::Char('k'), _) | (KeyCode::Up, _) => { + (KeyCode::Char('k') | KeyCode::Up, _) => { move_selection(inner, -1); KeyOutcome::Continue } @@ -486,7 +486,7 @@ pub(crate) fn refresh_docs(state: &mut App) -> anyhow::Result<()> { if len == 0 { state.library.inner.list_state.select(None); } else { - let next = prior.map(|p| p.min(len - 1)).unwrap_or(0); + let next = prior.map_or(0, |p| p.min(len - 1)); state.library.inner.list_state.select(Some(next)); } state.library.inner.needs_refresh = false; diff --git a/crates/kebab-tui/src/markdown.rs b/crates/kebab-tui/src/markdown.rs index 1b4409c..b8e65a6 100644 --- a/crates/kebab-tui/src/markdown.rs +++ b/crates/kebab-tui/src/markdown.rs @@ -221,7 +221,7 @@ pub fn render(text: &str, theme: &Theme) -> Vec> { // Render as `[^label]` so the footnote anchor is // visible in the answer body. current.push(Span::styled( - format!("[^{}]", label), + format!("[^{label}]"), theme.style(Role::CitationMarker), )); } @@ -463,13 +463,13 @@ mod tests { #[test] fn inline_and_display_math_render_as_text() { let inline = render("see $E = mc^2$ here", &theme()); - let combined: String = inline.iter().map(line_text).collect::>().join(""); + let combined: String = inline.iter().map(line_text).collect::(); assert!( combined.contains("E = mc^2"), "inline math content dropped: {combined:?}" ); let display = render("$$\\sum_i x_i$$", &theme()); - let combined: String = display.iter().map(line_text).collect::>().join(""); + let combined: String = display.iter().map(line_text).collect::(); assert!( combined.contains("\\sum_i x_i") || combined.contains("sum_i x_i"), "display math content dropped: {combined:?}" @@ -589,7 +589,7 @@ mod tests { #[test] fn unterminated_bold_does_not_drop_content() { let lines = render("**still typing", &theme()); - let combined: String = lines.iter().map(line_text).collect::>().join(""); + let combined: String = lines.iter().map(line_text).collect::(); assert!( combined.contains("still typing"), "stream-mid output dropped content text: {combined:?}" diff --git a/crates/kebab-tui/src/run.rs b/crates/kebab-tui/src/run.rs index cceca0f..f7a156c 100644 --- a/crates/kebab-tui/src/run.rs +++ b/crates/kebab-tui/src/run.rs @@ -36,8 +36,7 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> { let clear_now = app .ingest_state .as_ref() - .map(crate::ingest_progress::ready_to_clear) - .unwrap_or(false); + .is_some_and(crate::ingest_progress::ready_to_clear); if clear_now { if let Some(mut state) = app.ingest_state.take() { // Reap the worker thread now that the user has seen @@ -73,8 +72,7 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> { let due = app .search .as_ref() - .map(debounce_due) - .unwrap_or(false); + .is_some_and(debounce_due); if due { if let Err(e) = fire_search(app) { app.error_overlay = Some(ErrorOverlay::from_anyhow(&e)); @@ -84,8 +82,7 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> { let needs_preview = app .search .as_ref() - .map(|s| s.preview.is_none() && !s.hits.is_empty()) - .unwrap_or(false); + .is_some_and(|s| s.preview.is_none() && !s.hits.is_empty()); if needs_preview { if let Err(e) = refresh_preview(app) { app.error_overlay = Some(ErrorOverlay::from_anyhow(&e)); @@ -103,8 +100,7 @@ pub(crate) fn run_loop(app: &mut App) -> Result<()> { let due = app .inspect .as_ref() - .map(|s| s.needs_fetch) - .unwrap_or(false); + .is_some_and(|s| s.needs_fetch); if due { if let Err(e) = refresh_inspect(app) { app.error_overlay = Some(ErrorOverlay::from_anyhow(&e)); @@ -387,10 +383,10 @@ pub fn render_status_bar(f: &mut Frame, area: Rect, app: &App) { /// Priority-cascade dynamic state for the status bar. See /// `render_status_bar` for the priority order. fn dynamic_status(app: &App) -> String { - if app.ask.as_ref().map(|s| s.streaming).unwrap_or(false) { + if app.ask.as_ref().is_some_and(|s| s.streaming) { return "streaming…".to_string(); } - if app.search.as_ref().map(|s| s.searching).unwrap_or(false) { + if app.search.as_ref().is_some_and(|s| s.searching) { return "searching…".to_string(); } // v0.17.0 A5 Step 5: short-query advisory has higher priority than @@ -460,7 +456,7 @@ fn render_key_hints(f: &mut Frame, area: Rect, app: &App) { /// - **Order**: most-frequent verb first; last fragment is always /// the way back out (`Esc`/`q`). pub fn footer_hints(focus: Pane, mode: crate::app::Mode, filter_open: bool) -> &'static str { - use crate::app::Mode::*; + use crate::app::Mode::{Normal, Insert}; // p9-fb-21: every hint starts with `F1 도움말` so the cheatsheet // is always one keystroke away — dogfooding feedback was that // the F1 binding itself was undiscoverable. diff --git a/crates/kebab-tui/src/search.rs b/crates/kebab-tui/src/search.rs index 1e6bf75..782252c 100644 --- a/crates/kebab-tui/src/search.rs +++ b/crates/kebab-tui/src/search.rs @@ -239,16 +239,13 @@ pub fn handle_key_search(state: &mut App, key: KeyEvent) -> KeyOutcome { trace: true, ..Default::default() }; - match kebab_app::search_with_opts_with_config(state.config.clone(), q, opts) { - Ok(resp) => { - if let Some(t) = resp.trace { - state.trace_popup = Some(crate::trace_popup::TracePopupState::new(t)); - } - } - Err(_) => { - // Silent failure — trace is debug-only; user - // can still see search hits without it. + if let Ok(resp) = kebab_app::search_with_opts_with_config(state.config.clone(), q, opts) { + if let Some(t) = resp.trace { + state.trace_popup = Some(crate::trace_popup::TracePopupState::new(t)); } + } else { + // Silent failure — trace is debug-only; user + // can still see search hits without it. } } return KeyOutcome::Continue; @@ -481,9 +478,7 @@ pub fn build_jump_command( let mut args = leading_args; let editor_basename = std::path::Path::new(&program) - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .unwrap_or_else(|| program.clone()); + .file_name().map_or_else(|| program.clone(), |s| s.to_string_lossy().into_owned()); match citation { Citation::Line { start, .. } => { @@ -700,7 +695,7 @@ pub fn poll_worker(state: &mut App) { // input has drifted since spawn, the gen-check // already returned early. let q_text = - s.last_query.as_ref().map(|(t, _)| t.as_str()).unwrap_or(""); + s.last_query.as_ref().map_or("", |(t, _)| t.as_str()); s.short_query_hint = kebab_app::short_query_hint(q_text, hits.is_empty()); s.hits = hits; diff --git a/crates/kebab-tui/src/theme.rs b/crates/kebab-tui/src/theme.rs index ec7cd74..42f80ef 100644 --- a/crates/kebab-tui/src/theme.rs +++ b/crates/kebab-tui/src/theme.rs @@ -272,8 +272,7 @@ mod tests { let has_modifier = !style.add_modifier.is_empty(); assert!( has_color || has_modifier, - "role {:?} resolves to bare Style::default() in dark palette", - r + "role {r:?} resolves to bare Style::default() in dark palette" ); } } diff --git a/crates/kebab-tui/tests/ask.rs b/crates/kebab-tui/tests/ask.rs index 066ff7e..b40c222 100644 --- a/crates/kebab-tui/tests/ask.rs +++ b/crates/kebab-tui/tests/ask.rs @@ -1073,7 +1073,7 @@ fn input_pane_omits_multi_hop_badge_when_toggled_off() { rendered.contains("F2=multi-hop"), "title binding hint must always be visible; got:\n{rendered}" ); - let prompt_row = rendered.lines().find(|l| l.contains("?")).unwrap_or(""); + let prompt_row = rendered.lines().find(|l| l.contains('?')).unwrap_or(""); assert!( !prompt_row.contains("multi-hop"), "the badge belongs on the prompt row only when toggled on; got row:\n{prompt_row}" diff --git a/crates/kebab-tui/tests/search.rs b/crates/kebab-tui/tests/search.rs index 2a0119f..4e7fc29 100644 --- a/crates/kebab-tui/tests/search.rs +++ b/crates/kebab-tui/tests/search.rs @@ -33,7 +33,7 @@ fn fresh_app() -> App { fn make_hit(rank: u32, path: &str, snippet: &str, citation: Citation) -> SearchHit { SearchHit { rank, - chunk_id: ChunkId(format!("{:0<32}", rank)), + chunk_id: ChunkId(format!("{rank:0<32}")), doc_id: DocumentId(format!("{:0<32}", rank * 2)), doc_path: WorkspacePath::new(path.into()).unwrap(), heading_path: vec!["Section".into(), "Sub".into()],