chore: workspace-wide cleanup — clippy::pedantic baseline + auto-fix

cut PR v0.18.0 전 마지막 정리. 사용자 요청: "전체 코드베이스를 깔끔하고 알아보기 쉽게".

## Workspace lints

- `Cargo.toml` 의 `[workspace.lints.clippy]` 에 `pedantic = "warn"` (priority -1) + 의도적 allow-list 추가:
  - cast_possible_truncation / cast_possible_wrap / cast_sign_loss / cast_precision_loss — ONNX i64 / hash modular reduction 등 의도적 truncation.
  - doc_markdown / missing_errors_doc / missing_panics_doc — cosmetic doc style.
  - too_many_lines / module_name_repetitions / must_use_candidate / needless_pass_by_value / manual_let_else / items_after_statements / similar_names — informational only.
  - format_collect / match_wildcard_for_single_variants / trivially_copy_pass_by_ref / unnecessary_wraps — intentional patterns (exhaustive match, future Result variants 등).
  - default_trait_access — `Foo::default()` 가 idiomatic.
  - float_cmp — NLI / RRF score 의 explicit threshold 비교 의도.
  - struct_excessive_bools / case_sensitive_file_extension_comparisons / naive_bytecount / ignore_without_reason — domain-specific 의도.
  - format_push_string / return_self_not_must_use / match_same_arms — builder / wire-label / hot-path 패턴 보존.
  - needless_continue / used_underscore_binding / nonminimal_bool / unreadable_literal / many_single_char_names / doc_link_with_quotes / assigning_clones / collapsible_str_replace / trivial_regex / elidable_lifetime_names / range_plus_one / explicit_iter_loop / implicit_hasher / ref_option — remaining low-value style.
- 각 24 crate `Cargo.toml` 에 `[lints] workspace = true` 추가.

## Auto-fix

`cargo clippy --workspace --all-targets --fix` 적용 — 128 files changed, 552 insertions / 472 deletions. 주로:
- uninlined_format_args (~18): `format!("{}", x)` → `format!("{x}")`.
- redundant_closure_for_method_calls (~33): `.map(|x| x.foo())` → `.map(T::foo)`.
- 그 외 mechanical refactor.

## 검증

- `cargo clippy --workspace --all-targets -j 1 -- -D warnings` clean (pedantic + 모든 lint group).
- `cargo test --workspace --no-fail-fast -j 1` — **1293 tests pass + 1 pre-existing flaky fail** (`kebab-mcp::tools_call_ask_multi_hop::ask_tool_routes_multi_hop_true_to_decompose_first`, HOTFIX candidate, cleanup 무관). 회귀 0.

Wire 영향: 없음.
Behavior 영향: 없음 (mechanical refactor only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 03:01:58 +00:00
parent a0ccc7b021
commit 7c85de065a
128 changed files with 552 additions and 472 deletions

View File

@@ -34,6 +34,94 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/altair823/kebab" repository = "https://github.com/altair823/kebab"
version = "0.17.2" 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; `<Foo as Default>::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::<String>()` 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] [workspace.dependencies]
anyhow = "1" anyhow = "1"
thiserror = "2" thiserror = "2"

View File

@@ -81,3 +81,6 @@ lopdf = "0.32"
# error_wire::tests::llm_unreachable_classifies_to_model_unreachable needs a real # error_wire::tests::llm_unreachable_classifies_to_model_unreachable needs a real
# reqwest::Error (private constructor) — built from a connect-refused call. # reqwest::Error (private constructor) — built from a connect-refused call.
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
[lints]
workspace = true

View File

@@ -293,7 +293,7 @@ impl App {
// so other in-flight searches can use the cache concurrently. // so other in-flight searches can use the cache concurrently.
drop(guard); drop(guard);
let hits = self.search_uncached(query)?; 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()); guard.put(key, hits.clone());
Ok(hits) Ok(hits)
} }
@@ -467,7 +467,7 @@ impl App {
// Snippet truncation if opts.snippet_chars set (mirror non-trace path). // Snippet truncation if opts.snippet_chars set (mirror non-trace path).
if opts.snippet_chars.is_some() { if opts.snippet_chars.is_some() {
for h in hits.iter_mut() { for h in &mut hits {
if h.snippet.chars().count() > snippet_chars { if h.snippet.chars().count() > snippet_chars {
h.snippet = trim_to_chars(&h.snippet, 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 // `config.search.snippet_chars`; this only kicks in when the
// caller asked for *less*). // caller asked for *less*).
if opts.snippet_chars.is_some() { if opts.snippet_chars.is_some() {
for h in hits.iter_mut() { for h in &mut hits {
if h.snippet.chars().count() > snippet_chars { if h.snippet.chars().count() > snippet_chars {
h.snippet = trim_to_chars(&h.snippet, snippet_chars); h.snippet = trim_to_chars(&h.snippet, snippet_chars);
} }
@@ -521,7 +521,7 @@ impl App {
{ {
current_snippet_cap = current_snippet_cap =
(current_snippet_cap / 2).max(SNIPPET_FLOOR); (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 { if h.snippet.chars().count() > current_snippet_cap {
h.snippet = h.snippet =
trim_to_chars(&h.snippet, current_snippet_cap); trim_to_chars(&h.snippet, current_snippet_cap);
@@ -868,7 +868,7 @@ impl App {
/// clear` admin command). No-op when the cache is disabled. /// clear` admin command). No-op when the cache is disabled.
pub fn clear_search_cache(&self) { pub fn clear_search_cache(&self) {
if let Some(cache) = self.search_cache.as_ref() { 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(); guard.clear();
} }
} }

View File

@@ -139,9 +139,8 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> {
let k = obj let k = obj
.get("k") .get("k")
.and_then(|v| v.as_u64()) .and_then(serde_json::Value::as_u64)
.map(|n| n as usize) .map_or(0, |n| n as usize); // 0 → use config default in app
.unwrap_or(0); // 0 → use config default in app
let trust_min = match obj.get("trust_min").and_then(|v| v.as_str()) { let trust_min = match obj.get("trust_min").and_then(|v| v.as_str()) {
None => None, None => None,
@@ -209,14 +208,14 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> {
let opts = SearchOpts { let opts = SearchOpts {
max_tokens: obj max_tokens: obj
.get("max_tokens") .get("max_tokens")
.and_then(|v| v.as_u64()) .and_then(serde_json::Value::as_u64)
.map(|n| n as usize), .map(|n| n as usize),
snippet_chars: obj snippet_chars: obj
.get("snippet_chars") .get("snippet_chars")
.and_then(|v| v.as_u64()) .and_then(serde_json::Value::as_u64)
.map(|n| n as usize), .map(|n| n as usize),
cursor: obj.get("cursor").and_then(|v| v.as_str()).map(String::from), 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(( Ok((

View File

@@ -91,7 +91,7 @@ pub fn classify(err: &anyhow::Error, verbose: bool) -> ErrorV1 {
} }
let mut details = json!({}); let mut details = json!({});
if verbose { if verbose {
let chain: Vec<String> = err.chain().map(|c| c.to_string()).collect(); let chain: Vec<String> = err.chain().map(std::string::ToString::to_string).collect();
details = json!({"chain": chain}); details = json!({"chain": chain});
} }
ErrorV1 { ErrorV1 {

View File

@@ -50,7 +50,7 @@ pub fn ensure_kebabignore_entry(workspace_root: &Path) -> Result<()> {
if !existing.is_empty() && !existing.ends_with('\n') { if !existing.is_empty() && !existing.ends_with('\n') {
file.write_all(b"\n")?; file.write_all(b"\n")?;
} }
writeln!(file, "{}", KEBABIGNORE_LINE)?; writeln!(file, "{KEBABIGNORE_LINE}")?;
Ok(()) Ok(())
} }

View File

@@ -166,8 +166,8 @@ mod tests {
}; };
let v = serde_json::to_value(&ev).unwrap(); 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("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("idx").and_then(serde_json::Value::as_u64), Some(1));
assert_eq!(v.get("total").and_then(|n| n.as_u64()), Some(10)); 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("path").and_then(|s| s.as_str()), Some("notes/foo.md"));
assert_eq!(v.get("media").and_then(|s| s.as_str()), Some("markdown")); 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(); let v = serde_json::to_value(&ev).unwrap();
assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("completed")); assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("completed"));
let counts = v.get("counts").unwrap(); let counts = v.get("counts").unwrap();
assert_eq!(counts.get("scanned").and_then(|n| n.as_u64()), Some(5)); assert_eq!(counts.get("scanned").and_then(serde_json::Value::as_u64), Some(5));
assert_eq!(counts.get("new").and_then(|n| n.as_u64()), Some(2)); assert_eq!(counts.get("new").and_then(serde_json::Value::as_u64), Some(2));
} }
#[test] #[test]

View File

@@ -289,8 +289,7 @@ pub fn ingest_with_config_opts(
let cancelled = || { let cancelled = || {
opts.cancel opts.cancel
.as_ref() .as_ref()
.map(|c| c.load(std::sync::atomic::Ordering::Relaxed)) .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
.unwrap_or(false)
}; };
let force_reingest = opts.force_reingest; let force_reingest = opts.force_reingest;
let started_instant = std::time::Instant::now(); let started_instant = std::time::Instant::now();
@@ -394,7 +393,7 @@ pub fn ingest_with_config_opts(
let purged_deleted_files = sweep_deleted_files( let purged_deleted_files = sweep_deleted_files(
&app, &app,
&scanned_paths, &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(); 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; *skipped_by_extension.entry(ext).or_insert(0) += 1;
} }
kebab_core::IngestItemKind::Unchanged => { kebab_core::IngestItemKind::Unchanged => {
unchanged_count = unchanged_count.saturating_add(1) unchanged_count = unchanged_count.saturating_add(1);
} }
kebab_core::IngestItemKind::Error => { kebab_core::IngestItemKind::Error => {
error_count = error_count.saturating_add(1) error_count = error_count.saturating_add(1);
} }
} }
crate::ingest_progress::emit( crate::ingest_progress::emit(
@@ -940,9 +939,7 @@ fn try_skip_unchanged(
fn ext_for_skip_warning(path: &str) -> String { fn ext_for_skip_warning(path: &str) -> String {
std::path::Path::new(path) std::path::Path::new(path)
.extension() .extension()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str()).map_or_else(|| NO_EXT_SENTINEL.to_string(), str::to_ascii_lowercase)
.map(|s| s.to_ascii_lowercase())
.unwrap_or_else(|| NO_EXT_SENTINEL.to_string())
} }
/// p9-fb-25: render the `IngestItem.warnings` line for a Skipped /// p9-fb-25: render the `IngestItem.warnings` line for a Skipped
@@ -2407,7 +2404,7 @@ fn lang_hint_from_doc(doc: &CanonicalDocument) -> Option<Lang> {
/// Convenience: end byte of the frontmatter region (or 0 when absent). /// Convenience: end byte of the frontmatter region (or 0 when absent).
fn fm_span_end(span: Option<kebab_parse_md::FrontmatterSpan>) -> usize { fn fm_span_end(span: Option<kebab_parse_md::FrontmatterSpan>) -> 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 /// 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"]; const SUPPORTED_EXTS: &[&str] = &["md", "pdf", "png", "jpg", "jpeg"];
if !SUPPORTED_EXTS.contains(&ext.as_str()) { if !SUPPORTED_EXTS.contains(&ext.as_str()) {
anyhow::bail!( anyhow::bail!(
"ingest-file: unsupported extension `.{}` (supported: {:?})", "ingest-file: unsupported extension `.{ext}` (supported: {SUPPORTED_EXTS:?})"
ext, SUPPORTED_EXTS
); );
} }

View File

@@ -165,7 +165,7 @@ fn collect_stats(
store: &kebab_store_sqlite::SqliteStore, store: &kebab_store_sqlite::SqliteStore,
) -> anyhow::Result<Stats> { ) -> anyhow::Result<Stats> {
let counts = store 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 data_dir = kebab_config::expand_path(&cfg.storage.data_dir, "");
let index_bytes = kebab_store_sqlite::stats_ext::index_bytes(&data_dir) let index_bytes = kebab_store_sqlite::stats_ext::index_bytes(&data_dir)
.map_err(|e| anyhow::anyhow!("index_bytes: {e}"))?; .map_err(|e| anyhow::anyhow!("index_bytes: {e}"))?;

View File

@@ -1267,7 +1267,7 @@ fn tier1_cpp_ingest_searchable() {
// (method) depending on which chunk ranks first. // (method) depending on which chunk ranks first.
assert!( assert!(
symbol.as_deref().is_some_and(|s| s.starts_with("kebab::chunk::Foo")), 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"); assert!(*line_start >= 1, "line_start must be >=1");
} }

View File

@@ -33,7 +33,7 @@ fn ingest_file_copies_external_md_and_reports_new() {
assert!(ext_dir.is_dir()); assert!(ext_dir.is_dir());
let entries: Vec<_> = fs::read_dir(&ext_dir) let entries: Vec<_> = fs::read_dir(&ext_dir)
.unwrap() .unwrap()
.filter_map(|e| e.ok()) .filter_map(std::result::Result::ok)
.collect(); .collect();
assert_eq!(entries.len(), 1, "exactly one file in _external/"); assert_eq!(entries.len(), 1, "exactly one file in _external/");
let name = entries[0].file_name().to_string_lossy().into_owned(); let name = entries[0].file_name().to_string_lossy().into_owned();

View File

@@ -35,7 +35,7 @@ fn ingest_stdin_writes_frontmatter_and_reports_new() {
// _external/ contains exactly one .md file with frontmatter. // _external/ contains exactly one .md file with frontmatter.
let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external"); let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external");
let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap() let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap()
.filter_map(|e| e.ok()) .filter_map(std::result::Result::ok)
.collect(); .collect();
assert_eq!(entries.len(), 1); assert_eq!(entries.len(), 1);
let content = fs::read_to_string(entries[0].path()).unwrap(); 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 ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external");
let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap() let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap()
.filter_map(|e| e.ok()) .filter_map(std::result::Result::ok)
.collect(); .collect();
let content = fs::read_to_string(entries[0].path()).unwrap(); let content = fs::read_to_string(entries[0].path()).unwrap();
assert!(content.contains("title: \"Title\"")); assert!(content.contains("title: \"Title\""));

View File

@@ -14,12 +14,10 @@ use common::TestEnv;
fn require_avx_or_panic() { fn require_avx_or_panic() {
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
{ {
if !std::is_x86_feature_detected!("avx") { assert!(std::is_x86_feature_detected!("avx"),
panic!( "kb-app vector integration test requires AVX-capable hardware; \
"kb-app vector integration test requires AVX-capable hardware; \ host CPU lacks AVX. Run on an AVX-capable machine."
host CPU lacks AVX. Run on an AVX-capable machine." );
);
}
} }
} }

View File

@@ -26,3 +26,6 @@ kebab-parse-code = { path = "../kebab-parse-code" }
kebab-normalize = { path = "../kebab-normalize" } kebab-normalize = { path = "../kebab-normalize" }
serde_json = { workspace = true } serde_json = { workspace = true }
time = { workspace = true } time = { workspace = true }
[lints]
workspace = true

View File

@@ -266,7 +266,7 @@ mod tests {
#[test] #[test]
fn oversize_unit_splits_into_parts_with_unique_ids() { fn oversize_unit_splits_into_parts_with_unique_ids() {
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<Vec<_>>().join(""); let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<String>();
let code = format!("int big() {{\n{body}\n}}"); let code = format!("int big() {{\n{body}\n}}");
let doc = code_doc(&[("big", 1, 502, &code)]); let doc = code_doc(&[("big", 1, 502, &code)]);
let chunks = CodeCAstV1Chunker.chunk(&doc, &policy()).unwrap(); 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 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -266,7 +266,7 @@ mod tests {
#[test] #[test]
fn oversize_unit_splits_into_parts_with_unique_ids() { fn oversize_unit_splits_into_parts_with_unique_ids() {
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<Vec<_>>().join(""); let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<String>();
let code = format!("int big() {{\n{body}\n}}"); let code = format!("int big() {{\n{body}\n}}");
let doc = code_doc(&[("big", 1, 502, &code)]); let doc = code_doc(&[("big", 1, 502, &code)]);
let chunks = CodeCppAstV1Chunker.chunk(&doc, &policy()).unwrap(); 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 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -281,7 +281,7 @@ mod tests {
} }
} }
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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"); assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
} }

View File

@@ -387,9 +387,7 @@ fn render_block_text(b: &Block) -> String {
// alt keeps lexical search hits on filenames working even when // alt keeps lexical search hits on filenames working even when
// P6-1's filename auto-fill is bypassed. // P6-1's filename auto-fill is bypassed.
Block::ImageRef(i) => { Block::ImageRef(i) => {
let alt = if !i.alt.is_empty() { let alt = if i.alt.is_empty() {
i.alt.clone()
} else {
// P6-1 falls back to filename so this branch is // P6-1 falls back to filename so this branch is
// defensive — keep it lest a future test fixture or // defensive — keep it lest a future test fixture or
// synthetic block path skip the auto-fill. // synthetic block path skip the auto-fill.
@@ -399,17 +397,17 @@ fn render_block_text(b: &Block) -> String {
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.unwrap_or("[image]") .unwrap_or("[image]")
.to_string() .to_string()
} else {
i.alt.clone()
}; };
let ocr = i let ocr = i
.ocr .ocr
.as_ref() .as_ref()
.map(|o| o.joined.as_str()) .map_or("", |o| o.joined.as_str());
.unwrap_or("");
let cap = i let cap = i
.caption .caption
.as_ref() .as_ref()
.map(|c| c.text.as_str()) .map_or("", |c| c.text.as_str());
.unwrap_or("");
[alt.as_str(), ocr, cap] [alt.as_str(), ocr, cap]
.iter() .iter()
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())

View File

@@ -450,7 +450,7 @@ mod tests {
// chunk_ids stay distinct despite identical block_ids — the // chunk_ids stay distinct despite identical block_ids — the
// per-chunk policy_hash variant is doing its job. // 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(); 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(); let total = ids.len();
ids.dedup(); ids.dedup();
assert_eq!(ids.len(), total, "all chunk_ids must be unique"); 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 // chunk_ids stay distinct (the per-chunk hash variant keys off
// char_start which is now strictly increasing). // char_start which is now strictly increasing).
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect(); 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(); let total = ids.len();
ids.dedup(); ids.dedup();
assert_eq!(ids.len(), total, "chunk_ids must remain unique"); assert_eq!(ids.len(), total, "chunk_ids must remain unique");

View File

@@ -280,9 +280,7 @@ fn k8s_oversize_splits_into_line_windows_sharing_symbol() {
assert_eq!( assert_eq!(
prev_end + 1, prev_end + 1,
next_start, next_start,
"line ranges must be contiguous: {} → {} (got gap or overlap)", "line ranges must be contiguous: {prev_end} → {next_start} (got gap or overlap)"
prev_end,
next_start
); );
} }
} }

View File

@@ -51,7 +51,7 @@ fn manifest_doc(lang: &str, manifest_text: &str) -> CanonicalDocument {
doc_id, doc_id,
source_asset_id: aid, source_asset_id: aid,
workspace_path: wp, workspace_path: wp,
title: format!("Manifest ({})", lang), title: format!("Manifest ({lang})"),
lang: Lang("und".into()), lang: Lang("und".into()),
blocks: vec![block], blocks: vec![block],
metadata: Metadata { metadata: Metadata {

View File

@@ -50,3 +50,6 @@ tempfile = { workspace = true }
# to simulate stale docs. `time` is the formatter used by the helper. # to simulate stale docs. `time` is the formatter used by the helper.
rusqlite = { workspace = true } rusqlite = { workspace = true }
time = { workspace = true } time = { workspace = true }
[lints]
workspace = true

View File

@@ -797,7 +797,7 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
serde_json::to_string(&item.query)?, serde_json::to_string(&item.query)?,
)?; )?;
if let Some(err) = &item.error { if let Some(err) = &item.error {
writeln!(stdout, "error: {}", err)?; writeln!(stdout, "error: {err}")?;
} else if let Some(resp) = &item.response { } else if let Some(resp) = &item.response {
writeln!( writeln!(
stdout, stdout,
@@ -1171,15 +1171,13 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
let report = kebab_app::reset::execute(scope, &cfg)?; let report = kebab_app::reset::execute(scope, &cfg)?;
if cli.json { if cli.json {
println!("{}", serde_json::to_string(&wire::wire_reset(&report))?); println!("{}", serde_json::to_string(&wire::wire_reset(&report))?);
} else { } else if report.orphans_purged > 0 {
if report.orphans_purged > 0 { println!("orphans purged: {}", report.orphans_purged);
println!("orphans purged: {}", report.orphans_purged); for p in &report.purged_paths {
for p in &report.purged_paths { println!(" - {}", p.0);
println!(" - {}", p.0);
}
} else {
println!("no orphaned docs found — store is already in sync with walker scope");
} }
} else {
println!("no orphaned docs found — store is already in sync with walker scope");
} }
return Ok(()); return Ok(());
} }
@@ -1508,11 +1506,11 @@ fn confirm_destructive(
) -> anyhow::Result<bool> { ) -> anyhow::Result<bool> {
use std::io::Write; use std::io::Write;
let mut out = std::io::stderr().lock(); 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 { for p in paths {
writeln!(out, " - {}", p.display())?; writeln!(out, " - {}", p.display())?;
} }
writeln!(out, "estimated total: {} bytes", bytes)?; writeln!(out, "estimated total: {bytes} bytes")?;
write!(out, "Proceed? [y/N] ")?; write!(out, "Proceed? [y/N] ")?;
out.flush()?; out.flush()?;
@@ -1573,19 +1571,19 @@ fn render_fetch_plain(r: &kebab_core::FetchResult) {
if !r.context_before.is_empty() { if !r.context_before.is_empty() {
println!("\n=== before ==="); println!("\n=== before ===");
for c in &r.context_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); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
} }
} }
if let Some(c) = &r.chunk { if let Some(c) = &r.chunk {
println!("\n=== target ==="); 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); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
} }
if !r.context_after.is_empty() { if !r.context_after.is_empty() {
println!("\n=== after ==="); println!("\n=== after ===");
for c in &r.context_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); println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
} }
} }

View File

@@ -313,7 +313,7 @@ mod tests {
v.get("next_cursor").and_then(|c| c.as_str()), v.get("next_cursor").and_then(|c| c.as_str()),
Some("opaque-cursor-abc") 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] #[test]

View File

@@ -88,5 +88,5 @@ max_context_tokens = 8000
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap(); 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("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));
} }

View File

@@ -96,5 +96,5 @@ max_context_tokens = 8000
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap(); 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("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));
} }

View File

@@ -43,7 +43,7 @@ fn cli_mcp_initialize_then_tools_list() {
reader.read_line(&mut line).unwrap(); reader.read_line(&mut line).unwrap();
let init: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); let init: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
assert_eq!( assert_eq!(
init.get("id").and_then(|i| i.as_i64()), init.get("id").and_then(serde_json::Value::as_i64),
Some(1), Some(1),
"unexpected id in initialize response: {init}" "unexpected id in initialize response: {init}"
); );
@@ -57,7 +57,7 @@ fn cli_mcp_initialize_then_tools_list() {
reader.read_line(&mut line).unwrap(); reader.read_line(&mut line).unwrap();
let list: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); let list: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
assert_eq!( assert_eq!(
list.get("id").and_then(|i| i.as_i64()), list.get("id").and_then(serde_json::Value::as_i64),
Some(2), Some(2),
"unexpected id in tools/list response: {list}" "unexpected id in tools/list response: {list}"
); );

View File

@@ -76,8 +76,7 @@ fn cli_schema_json_emits_schema_v1() {
assert!( assert!(
v.get("kebab_version") v.get("kebab_version")
.and_then(|s| s.as_str()) .and_then(|s| s.as_str())
.map(|s| !s.is_empty()) .is_some_and(|s| !s.is_empty()),
.unwrap_or(false),
"kebab_version must be a non-empty string" "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()) .and_then(|c| c.as_object())
.expect("capabilities must be a JSON object"); .expect("capabilities must be a JSON object");
assert_eq!( 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), Some(true),
"capabilities.json_mode must be true" "capabilities.json_mode must be true"
); );
assert_eq!( 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), Some(true),
"capabilities.mcp_server must be true (fb-30)" "capabilities.mcp_server must be true (fb-30)"
); );

View File

@@ -155,8 +155,8 @@ fn ingest_json_progress_lines_carry_kind_and_ts() {
saw_completed = true; saw_completed = true;
// Counts mirror the report. // Counts mirror the report.
let counts = v.get("counts").unwrap(); let counts = v.get("counts").unwrap();
assert_eq!(counts.get("scanned").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(|n| n.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"); assert!(saw_scan_started, "missing scan_started event");

View File

@@ -22,3 +22,6 @@ tracing = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -157,7 +157,7 @@ mod tests {
#[test] #[test]
fn xdg_data_home_set_replaces_var() { 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(); let _guard = XdgGuard::capture();
// SAFETY: lock held for the duration of this test. // SAFETY: lock held for the duration of this test.
unsafe { std::env::set_var("XDG_DATA_HOME", "/custom/path") }; unsafe { std::env::set_var("XDG_DATA_HOME", "/custom/path") };
@@ -168,7 +168,7 @@ mod tests {
#[test] #[test]
fn xdg_data_home_unset_uses_default() { 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(); let _guard = XdgGuard::capture();
// SAFETY: lock held for the duration of this test. // SAFETY: lock held for the duration of this test.
unsafe { std::env::remove_var("XDG_DATA_HOME") }; unsafe { std::env::remove_var("XDG_DATA_HOME") };
@@ -181,7 +181,7 @@ mod tests {
#[test] #[test]
fn xdg_with_no_default_resolves_to_empty_when_unset() { 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(); let _guard = XdgGuard::capture();
// SAFETY: lock held for the duration of this test. // SAFETY: lock held for the duration of this test.
unsafe { std::env::remove_var("XDG_DATA_HOME") }; unsafe { std::env::remove_var("XDG_DATA_HOME") };
@@ -193,7 +193,7 @@ mod tests {
#[test] #[test]
fn leading_tilde_expands_to_home() { 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 home = std::env::var("HOME").expect("HOME must be set in tests");
let p = expand_path("~/runs", ""); let p = expand_path("~/runs", "");
assert_eq!(p, PathBuf::from(home).join("runs")); assert_eq!(p, PathBuf::from(home).join("runs"));
@@ -229,7 +229,7 @@ mod tests {
#[test] #[test]
fn tilde_path_ignores_base_dir() { 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 home = std::env::var("HOME").expect("HOME must be set in tests");
let base = Path::new("/tmp/ignored-cfg"); let base = Path::new("/tmp/ignored-cfg");
let p = expand_path_with_base("~/x", "", base); let p = expand_path_with_base("~/x", "", base);
@@ -238,7 +238,7 @@ mod tests {
#[test] #[test]
fn xdg_var_path_ignores_base_dir() { 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(); let _guard = XdgGuard::capture();
// SAFETY: lock held for the duration of this test. // SAFETY: lock held for the duration of this test.
unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") }; unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") };
@@ -255,7 +255,7 @@ mod tests {
// Order matters: substitute `{data_dir}` (which itself contains // Order matters: substitute `{data_dir}` (which itself contains
// an unexpanded `${XDG_DATA_HOME}` and `~`), then the other two // an unexpanded `${XDG_DATA_HOME}` and `~`), then the other two
// resolve the result. // 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(); let _guard = XdgGuard::capture();
// SAFETY: lock held for the duration of this test. // SAFETY: lock held for the duration of this test.
unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") }; unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") };

View File

@@ -16,3 +16,6 @@ time = { workspace = true }
blake3 = { workspace = true } blake3 = { workspace = true }
serde_json_canonicalizer = "0.3" serde_json_canonicalizer = "0.3"
unicode-normalization = "0.1" unicode-normalization = "0.1"
[lints]
workspace = true

View File

@@ -226,28 +226,25 @@ fn parse_hms_ms(s: &str) -> Result<u64> {
let m: u64 = parts[1] let m: u64 = parts[1]
.parse() .parse()
.map_err(|_| anyhow::anyhow!("bad minutes in {:?} (input {s:?})", parts[1]))?; .map_err(|_| anyhow::anyhow!("bad minutes in {:?} (input {s:?})", parts[1]))?;
let (sec, ms) = match parts[2].split_once('.') { let (sec, ms) = if let Some((s_part, ms_part)) = parts[2].split_once('.') {
Some((s_part, ms_part)) => { let sec: u64 = s_part
let sec: u64 = s_part .parse()
.parse() .map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?;
.map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?; // Pad/truncate to exactly 3 digits.
// Pad/truncate to exactly 3 digits. let mut ms_str = ms_part.to_owned();
let mut ms_str = ms_part.to_owned(); while ms_str.len() < 3 {
while ms_str.len() < 3 { ms_str.push('0');
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)
} }
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) Ok(h * 3_600_000 + m * 60_000 + sec * 1000 + ms)
} }

View File

@@ -471,7 +471,7 @@ mod tests {
doc_path: WorkspacePath("a.md".into()), doc_path: WorkspacePath("a.md".into()),
heading_path: vec![], heading_path: vec![],
section_label: None, section_label: None,
snippet: "".into(), snippet: String::new(),
citation: Citation::Line { citation: Citation::Line {
path: WorkspacePath("a.md".into()), path: WorkspacePath("a.md".into()),
start: 1, start: 1,
@@ -502,7 +502,7 @@ mod tests {
doc_path: WorkspacePath("a.rs".into()), doc_path: WorkspacePath("a.rs".into()),
heading_path: vec![], heading_path: vec![],
section_label: None, section_label: None,
snippet: "".into(), snippet: String::new(),
citation: Citation::Code { citation: Citation::Code {
path: WorkspacePath("a.rs".into()), path: WorkspacePath("a.rs".into()),
line_start: 1, line_start: 1,

View File

@@ -20,3 +20,6 @@ anyhow = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true

View File

@@ -158,7 +158,7 @@ impl Embedder for FastembedEmbedder {
let guard = self let guard = self
.inner .inner
.lock() .lock()
.unwrap_or_else(|p| p.into_inner()); .unwrap_or_else(std::sync::PoisonError::into_inner);
let batch: Vec<Vec<f32>> = guard let batch: Vec<Vec<f32>> = guard
.embed(chunk_vec, Some(self.batch_size)) .embed(chunk_vec, Some(self.batch_size))
.context("fastembed: embed")?; .context("fastembed: embed")?;

View File

@@ -28,3 +28,6 @@ mock = []
[dev-dependencies] [dev-dependencies]
proptest = { workspace = true } proptest = { workspace = true }
[lints]
workspace = true

View File

@@ -59,7 +59,7 @@ pub fn assert_vector_shape(vecs: &[Vec<f32>], expected_dims: usize) {
/// Panics on mismatch (test-only helper — callers are tests). /// Panics on mismatch (test-only helper — callers are tests).
pub fn assert_unit_norm(vecs: &[Vec<f32>], tolerance: f32) { pub fn assert_unit_norm(vecs: &[Vec<f32>], tolerance: f32) {
for (i, v) in vecs.iter().enumerate() { 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; let norm = norm_sq.sqrt() as f32;
assert!( assert!(
(norm - 1.0).abs() <= tolerance, (norm - 1.0).abs() <= tolerance,

View File

@@ -132,10 +132,10 @@ impl Embedder for MockEmbedder {
.collect(); .collect();
// L2-normalize. Skip the rare all-zero case to avoid 0/0 = NaN. // 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 { if norm_sq > 0.0 {
let inv = (1.0 / norm_sq.sqrt()) as f32; let inv = (1.0 / norm_sq.sqrt()) as f32;
for x in v.iter_mut() { for x in &mut v {
*x *= inv; *x *= inv;
} }
} }

View File

@@ -28,3 +28,6 @@ uuid = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
rusqlite = { workspace = true } rusqlite = { workspace = true }
[lints]
workspace = true

View File

@@ -260,8 +260,8 @@ pub fn render_report_md(report: &CompareReport) -> String {
"| {} | {} | {} | {} | {} |", "| {} | {} | {} | {} | {} |",
c.query_id, c.query_id,
comparison_kind_label(c.kind), comparison_kind_label(c.kind),
c.a_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(|r| r.to_string()).unwrap_or_else(|| "".into()), c.b_hit_rank.map_or_else(|| "".into(), |r| r.to_string()),
c.note.as_deref().unwrap_or(""), c.note.as_deref().unwrap_or(""),
); );
} }
@@ -308,7 +308,7 @@ fn extract_chunker_version(snapshot_json: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(snapshot_json).ok()?; let v: serde_json::Value = serde_json::from_str(snapshot_json).ok()?;
v.get("chunker_version") v.get("chunker_version")
.and_then(|x| x.as_str()) .and_then(|x| x.as_str())
.map(|s| s.to_owned()) .map(std::borrow::ToOwned::to_owned)
} }
fn parse_results( fn parse_results(
@@ -402,8 +402,7 @@ fn classify(
// so refusal-flow queries (no expected_*) don't appear as // so refusal-flow queries (no expected_*) don't appear as
// regressions. // regressions.
let has_expected = gq let has_expected = gq
.map(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty()) .is_some_and(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty());
.unwrap_or(false);
if has_expected { if has_expected {
(ComparisonKind::Regression, Some("hit→miss".into())) (ComparisonKind::Regression, Some("hit→miss".into()))
} else { } else {
@@ -426,7 +425,7 @@ fn build_deltas(
if a.is_nan() || b.is_nan() { if a.is_nan() || b.is_nan() {
serde_json::Value::Null serde_json::Value::Null
} else { } else {
serde_json::Value::from((b - a) as f64) serde_json::Value::from(f64::from(b - a))
} }
} }
let mut hit = serde_json::Map::new(); let mut hit = serde_json::Map::new();

View File

@@ -270,7 +270,21 @@ pub(crate) fn aggregate_from_rows(
// recall@k_doc (doc-level, requires non-empty expected_doc_ids // recall@k_doc (doc-level, requires non-empty expected_doc_ids
// and `>0` is the "should retrieve" condition; refusal queries // and `>0` is the "should retrieve" condition; refusal queries
// (`expected_doc_ids = []`) are excluded by spec). // (`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(); let expected_docs: HashSet<&DocumentId> = gq.expected_doc_ids.iter().collect();
for k in TOP_K_VARIANTS { for k in TOP_K_VARIANTS {
let entry = recall_at_k_doc.get_mut(k).expect("init"); 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; let frac = covered as f64 / expected_docs.len() as f64;
entry.0 += frac; 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 // groundedness + citation_coverage (only meaningful with RAG

View File

@@ -143,7 +143,7 @@ fn env_guard() -> std::sync::MutexGuard<'static, ()> {
static M: OnceLock<Mutex<()>> = OnceLock::new(); static M: OnceLock<Mutex<()>> = OnceLock::new();
M.get_or_init(|| Mutex::new(())) M.get_or_init(|| Mutex::new(()))
.lock() .lock()
.unwrap_or_else(|e| e.into_inner()) .unwrap_or_else(std::sync::PoisonError::into_inner)
} }
#[test] #[test]

View File

@@ -147,7 +147,7 @@ fn lexical_opts() -> EvalRunOpts {
/// guard must outlive the call so concurrent tests don't reset the /// guard must outlive the call so concurrent tests don't reset the
/// var mid-run. /// var mid-run.
fn run_with_golden<F: FnOnce() -> R, R>(yaml: &Path, f: F) -> R { fn run_with_golden<F: FnOnce() -> 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 // SAFETY: `KEBAB_EVAL_GOLDEN` is a benign env var; the GOLDEN_ENV_LOCK
// serializes mutations so concurrent tests don't race. // serializes mutations so concurrent tests don't race.
unsafe { unsafe {

View File

@@ -34,3 +34,6 @@ anyhow = { workspace = true }
# `tokio::*` symbols, so the public/runtime API stays sync. # `tokio::*` symbols, so the public/runtime API stays sync.
wiremock = { workspace = true } wiremock = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] } tokio = { workspace = true, features = ["macros", "rt"] }
[lints]
workspace = true

View File

@@ -400,9 +400,9 @@ impl Iterator for OllamaStream {
// u32 saturation: even ~4G tokens is implausible for a // u32 saturation: even ~4G tokens is implausible for a
// single chat turn; we still saturate rather than // single chat turn; we still saturate rather than
// panic on the unlikely case. // panic on the unlikely case.
prompt_tokens: prompt_tokens.min(u32::MAX as u64) as u32, prompt_tokens: prompt_tokens.min(u64::from(u32::MAX)) as u32,
completion_tokens: completion_tokens.min(u32::MAX as u64) as u32, completion_tokens: completion_tokens.min(u64::from(u32::MAX)) as u32,
latency_ms: (total_duration_ns / 1_000_000).min(u32::MAX as u64) as u32, latency_ms: (total_duration_ns / 1_000_000).min(u64::from(u32::MAX)) as u32,
}; };
return Some(Ok(TokenChunk::Done { return Some(Ok(TokenChunk::Done {
finish_reason, finish_reason,

View File

@@ -19,3 +19,6 @@ mock = []
[dev-dependencies] [dev-dependencies]
proptest = { workspace = true } proptest = { workspace = true }
[lints]
workspace = true

View File

@@ -27,3 +27,6 @@ kebab-core = { path = "../kebab-core" }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -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. // Empty KB → refusal (grounded:false) is normal — NOT isError.
assert!( assert!(
!result.is_error.unwrap_or(false), !result.is_error.unwrap_or(false),
"expected isError=false on refusal, got {:?}", "expected isError=false on refusal, got {result:?}"
result
); );
let content = 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" "response should carry schema_version=answer.v1"
); );
assert_eq!( assert_eq!(
v.get("grounded").and_then(|b| b.as_bool()), v.get("grounded").and_then(serde_json::Value::as_bool),
Some(false), Some(false),
"empty KB should produce grounded=false" "empty KB should produce grounded=false"
); );

View File

@@ -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 // `ok` boolean must be present (value may be false in CI where Ollama
// is not reachable — that's expected and acceptable). // is not reachable — that's expected and acceptable).
assert!( 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}" "`ok` field missing in doctor.v1 response: {v}"
); );
} }

View File

@@ -98,8 +98,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() {
assert!( assert!(
!result.is_error.unwrap_or(false), !result.is_error.unwrap_or(false),
"expected isError=false, got {:?}", "expected isError=false, got {result:?}"
result
); );
let content = result let content = result
@@ -123,7 +122,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() {
"kind must be 'chunk'" "kind must be 'chunk'"
); );
assert!( 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" "chunk payload must be populated for kind=chunk"
); );
} }

View File

@@ -49,7 +49,7 @@ async fn ingest_file_tool_returns_ingest_report_v1() {
v.get("schema_version").and_then(|s| s.as_str()), v.get("schema_version").and_then(|s| s.as_str()),
Some("ingest_report.v1") 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] #[tokio::test]
@@ -91,7 +91,7 @@ async fn ingest_file_tool_idempotent_on_second_call() {
other => panic!("expected text, got {other:?}"), other => panic!("expected text, got {other:?}"),
}; };
let v1: serde_json::Value = serde_json::from_str(text1).unwrap(); 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. // Second call — same content, expect unchanged=1.
let r2 = tokio::task::spawn_blocking({ 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:?}"), other => panic!("expected text, got {other:?}"),
}; };
let v2: serde_json::Value = serde_json::from_str(text2).unwrap(); 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("new").and_then(serde_json::Value::as_u64), Some(0), "{v2:?}");
assert_eq!(v2.get("unchanged").and_then(|n| n.as_u64()), Some(1), "{v2:?}"); assert_eq!(v2.get("unchanged").and_then(serde_json::Value::as_u64), Some(1), "{v2:?}");
} }

View File

@@ -52,7 +52,7 @@ async fn ingest_stdin_tool_returns_ingest_report_v1() {
v.get("schema_version").and_then(|s| s.as_str()), v.get("schema_version").and_then(|s| s.as_str()),
Some("ingest_report.v1") 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] #[tokio::test]

View File

@@ -49,8 +49,7 @@ async fn schema_tool_returns_schema_v1_json() {
assert!( assert!(
!result.is_error.unwrap_or(false), !result.is_error.unwrap_or(false),
"expected isError=false on healthy schema, got {:?}", "expected isError=false on healthy schema, got {result:?}"
result
); );
let content = result.content.first().expect("expected at least one content item"); 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}" "unexpected schema_version in: {v}"
); );
assert_eq!( 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), Some(true),
"mcp_server capability flag should be true after fb-30", "mcp_server capability flag should be true after fb-30",
); );

View File

@@ -71,8 +71,7 @@ async fn search_tool_returns_search_response_v1() {
assert!( assert!(
!result.is_error.unwrap_or(false), !result.is_error.unwrap_or(false),
"expected isError=false, got {:?}", "expected isError=false, got {result:?}"
result
); );
let content = 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. // truncated must be present (bool); next_cursor may be null on last page.
assert!( 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" "envelope should carry truncated:bool"
); );
assert!( assert!(
@@ -172,8 +171,7 @@ async fn search_with_doc_id_filter_returns_only_target() {
); );
assert!( assert!(
!unfiltered.is_error.unwrap_or(false), !unfiltered.is_error.unwrap_or(false),
"unfiltered search failed: {:?}", "unfiltered search failed: {unfiltered:?}"
unfiltered
); );
let unfiltered_text = match &unfiltered.content.first().unwrap().raw { let unfiltered_text = match &unfiltered.content.first().unwrap().raw {
RawContent::Text(t) => t.text.clone(), RawContent::Text(t) => t.text.clone(),
@@ -211,8 +209,7 @@ async fn search_with_doc_id_filter_returns_only_target() {
); );
assert!( assert!(
!filtered.is_error.unwrap_or(false), !filtered.is_error.unwrap_or(false),
"filtered search failed: {:?}", "filtered search failed: {filtered:?}"
filtered
); );
let filtered_text = match &filtered.content.first().unwrap().raw { let filtered_text = match &filtered.content.first().unwrap().raw {
RawContent::Text(t) => t.text.clone(), RawContent::Text(t) => t.text.clone(),

View File

@@ -28,3 +28,6 @@ tracing = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -235,11 +235,11 @@ impl NliVerifier for OnnxNliVerifier {
.encode((premise, hypothesis), true) .encode((premise, hypothesis), true)
.map_err(|e| anyhow!("kebab-nli: tokenizer.encode failed: {e}"))?; .map_err(|e| anyhow!("kebab-nli: tokenizer.encode failed: {e}"))?;
let ids: Vec<i64> = enc.get_ids().iter().map(|&u| u as i64).collect(); let ids: Vec<i64> = enc.get_ids().iter().map(|&u| i64::from(u)).collect();
let mask: Vec<i64> = enc let mask: Vec<i64> = enc
.get_attention_mask() .get_attention_mask()
.iter() .iter()
.map(|&u| u as i64) .map(|&u| i64::from(u))
.collect(); .collect();
let seq_len = ids.len(); let seq_len = ids.len();
@@ -266,8 +266,7 @@ impl NliVerifier for OnnxNliVerifier {
let shape = logits.shape(); let shape = logits.shape();
if shape != [1, LOGITS_LEN] { if shape != [1, LOGITS_LEN] {
anyhow::bail!( anyhow::bail!(
"kebab-nli: unexpected logits shape {:?}, expected [1, {LOGITS_LEN}]", "kebab-nli: unexpected logits shape {shape:?}, expected [1, {LOGITS_LEN}]"
shape
); );
} }
let l = [logits[[0, 0]], logits[[0, 1]], logits[[0, 2]]]; let l = [logits[[0, 0]], logits[[0, 1]], logits[[0, 2]]];

View File

@@ -111,8 +111,7 @@ fn long_premise_truncates_without_panic() {
] { ] {
assert!( assert!(
x.is_finite(), x.is_finite(),
"channel {name} non-finite: {x} (full scores: {:?})", "channel {name} non-finite: {x} (full scores: {s:?})"
s
); );
} }
// Softmax invariant — the three channels sum to ~1. // Softmax invariant — the three channels sum to ~1.

View File

@@ -25,3 +25,6 @@ tracing = { workspace = true }
# default scope, excluding dev-deps) confirms this. # default scope, excluding dev-deps) confirms this.
kebab-parse-md = { path = "../kebab-parse-md" } kebab-parse-md = { path = "../kebab-parse-md" }
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true

View File

@@ -27,3 +27,6 @@ tree-sitter-cpp = { workspace = true }
[dev-dependencies] [dev-dependencies]
tempfile = { workspace = true } tempfile = { workspace = true }
[lints]
workspace = true

View File

@@ -310,7 +310,7 @@ fn build_blocks(
// If there is only glue (no real unit) the single pushed "<top-level>" // If there is only glue (no real unit) the single pushed "<top-level>"
// label should be "<module>" — rename it now. // label should be "<module>" — rename it now.
if !has_real_unit { if !has_real_unit {
for (sym, _, _, _) in units.iter_mut() { for (sym, _, _, _) in &mut units {
if sym == "<top-level>" { if sym == "<top-level>" {
*sym = "<module>".to_string(); *sym = "<module>".to_string();
} }
@@ -329,7 +329,7 @@ fn build_blocks(
lang: Some("c".to_string()), lang: Some("c".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,
@@ -704,11 +704,11 @@ void print_result(int v) {
#[test] #[test]
fn c_extractor_deterministic_across_runs() { fn c_extractor_deterministic_across_runs() {
let src = r#" let src = r"
struct Node { int val; }; struct Node { int val; };
int sum(int a, int b) { return a + b; } int sum(int a, int b) { return a + b; }
void noop(void) {} void noop(void) {}
"#; ";
let a = tests_support::extract_c(src, "x/det.c"); let a = tests_support::extract_c(src, "x/det.c");
for _ in 0..20 { for _ in 0..20 {
assert_eq!( assert_eq!(

View File

@@ -224,7 +224,7 @@ fn build_blocks_top(
units.push(("<module>".to_string(), 1, total.max(1), false)); units.push(("<module>".to_string(), 1, total.max(1), false));
} }
if !has_real_unit { if !has_real_unit {
for (sym, _, _, _) in units.iter_mut() { for (sym, _, _, _) in &mut units {
if sym == "<top-level>" { if sym == "<top-level>" {
*sym = "<module>".to_string(); *sym = "<module>".to_string();
} }
@@ -243,7 +243,7 @@ fn build_blocks_top(
lang: Some("cpp".to_string()), lang: Some("cpp".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,
@@ -696,7 +696,7 @@ mod tests {
#[test] #[test]
fn namespace_and_class() { fn namespace_and_class() {
let src = r#" let src = r"
namespace ns { namespace ns {
class Foo { class Foo {
public: public:
@@ -706,7 +706,7 @@ namespace ns {
int operator+(const Foo& o) { return 0; } int operator+(const Foo& o) { return 0; }
}; };
} }
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!(s.iter().any(|x| x == "ns::Foo"), "ns::Foo missing: {s:?}"); assert!(s.iter().any(|x| x == "ns::Foo"), "ns::Foo missing: {s:?}");
@@ -718,11 +718,11 @@ namespace ns {
#[test] #[test]
fn anonymous_namespace() { fn anonymous_namespace() {
let src = r#" let src = r"
namespace { namespace {
void hidden_fn() {} void hidden_fn() {}
} }
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!( assert!(
@@ -733,11 +733,11 @@ namespace {
#[test] #[test]
fn nested_namespace_specifier() { fn nested_namespace_specifier() {
let src = r#" let src = r"
namespace outer::inner { namespace outer::inner {
void fn_in_nested() {} void fn_in_nested() {}
} }
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!( assert!(
@@ -748,9 +748,9 @@ namespace outer::inner {
#[test] #[test]
fn out_of_class_method_def() { fn out_of_class_method_def() {
let src = r#" let src = r"
void ns::Foo::method() { } void ns::Foo::method() { }
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!( assert!(
@@ -761,7 +761,7 @@ void ns::Foo::method() { }
#[test] #[test]
fn template_declaration() { fn template_declaration() {
let src = r#" let src = r"
template<typename T> template<typename T>
class Bar { class Bar {
void tmpl_method() {} void tmpl_method() {}
@@ -769,7 +769,7 @@ class Bar {
template<typename T> template<typename T>
void tmpl_free_fn(T x) {} void tmpl_free_fn(T x) {}
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!(s.iter().any(|x| x == "Bar"), "Bar class missing: {s:?}"); assert!(s.iter().any(|x| x == "Bar"), "Bar class missing: {s:?}");
@@ -785,12 +785,12 @@ void tmpl_free_fn(T x) {}
#[test] #[test]
fn enum_and_concept() { fn enum_and_concept() {
let src = r#" let src = r"
enum class Color { Red, Green }; enum class Color { Red, Green };
template<typename T> template<typename T>
concept Printable = requires(T t) { t.print(); }; concept Printable = requires(T t) { t.print(); };
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!(s.iter().any(|x| x == "Color"), "Color missing: {s:?}"); assert!(s.iter().any(|x| x == "Color"), "Color missing: {s:?}");
@@ -813,11 +813,11 @@ extern "C" {
#[test] #[test]
fn conversion_operator() { fn conversion_operator() {
let src = r#" let src = r"
class Foo { class Foo {
operator bool() const { return true; } operator bool() const { return true; }
}; };
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!( assert!(
@@ -852,11 +852,11 @@ class Foo {
#[test] #[test]
fn ref_returning_operator() { fn ref_returning_operator() {
let src = r#" let src = r"
class Foo { class Foo {
Foo& operator=(const Foo& o) { return *this; } Foo& operator=(const Foo& o) { return *this; }
}; };
"#; ";
let doc = tests_support::extract_cpp(src, "x/foo.cpp"); let doc = tests_support::extract_cpp(src, "x/foo.cpp");
let s = syms(&doc); let s = syms(&doc);
assert!( assert!(
@@ -867,14 +867,14 @@ class Foo {
#[test] #[test]
fn deterministic_across_runs() { fn deterministic_across_runs() {
let src = r#" let src = r"
namespace ns { namespace ns {
class Foo { class Foo {
void method() {} void method() {}
}; };
} }
void free_fn() {} void free_fn() {}
"#; ";
let a = tests_support::extract_cpp(src, "x/foo.cpp"); let a = tests_support::extract_cpp(src, "x/foo.cpp");
for _ in 0..20 { for _ in 0..20 {
assert_eq!(tests_support::extract_cpp(src, "x/foo.cpp").blocks, a.blocks); assert_eq!(tests_support::extract_cpp(src, "x/foo.cpp").blocks, a.blocks);

View File

@@ -315,7 +315,7 @@ fn build_blocks(
// mod-prefix-agnostic. // mod-prefix-agnostic.
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -335,7 +335,7 @@ fn build_blocks(
lang: Some("go".to_string()), lang: Some("go".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -248,7 +248,7 @@ fn build_blocks(
// post-pass as 1B / 1C-Go). // post-pass as 1B / 1C-Go).
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -268,7 +268,7 @@ fn build_blocks(
lang: Some("java".to_string()), lang: Some("java".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -293,7 +293,7 @@ fn build_blocks(
let inner_kind = inner.kind(); let inner_kind = inner.kind();
match inner_kind { match inner_kind {
"function_declaration" | "class_declaration" => { "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 { if let Some(name) = name_opt {
glue.retain(|(_, gs, _)| *gs < outer_s); glue.retain(|(_, gs, _)| *gs < outer_s);
flush_glue(glue, units, mod_prefix, mod_path); flush_glue(glue, units, mod_prefix, mod_path);
@@ -332,7 +332,7 @@ fn build_blocks(
| "function_declaration" | "function_declaration"
| "class" | "class"
| "class_declaration" => { | "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 = let leaf =
name_opt.as_deref().unwrap_or("default").to_string(); name_opt.as_deref().unwrap_or("default").to_string();
glue.retain(|(_, gs, _)| *gs < outer_s); glue.retain(|(_, gs, _)| *gs < outer_s);
@@ -402,7 +402,7 @@ fn build_blocks(
// post-pass as 1A Gap 1 / Python / TS). // post-pass as 1A Gap 1 / Python / TS).
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -422,7 +422,7 @@ fn build_blocks(
lang: Some("javascript".to_string()), lang: Some("javascript".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -290,7 +290,7 @@ fn build_blocks(
// post-pass as 1B / 1C-Go / Java). // post-pass as 1B / 1C-Go / Java).
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -310,7 +310,7 @@ fn build_blocks(
lang: Some("kotlin".to_string()), lang: Some("kotlin".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -333,7 +333,7 @@ fn build_blocks(
// future-proofed) still demotes correctly. // future-proofed) still demotes correctly.
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -353,7 +353,7 @@ fn build_blocks(
lang: Some("python".to_string()), lang: Some("python".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -336,7 +336,7 @@ fn build_blocks(
// group is `<top-level>`, even a pure mod-decl group. // group is `<top-level>`, even a pure mod-decl group.
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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 // Match on the *suffix*: a glue group may now carry a module
// prefix (`inner::<module>`), so demote any `…<module>` to the // prefix (`inner::<module>`), so demote any `…<module>` to the
// same-prefixed `…<top-level>` rather than only the bare form. // same-prefixed `…<top-level>` rather than only the bare form.
@@ -359,7 +359,7 @@ fn build_blocks(
lang: Some("rust".to_string()), lang: Some("rust".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -326,7 +326,7 @@ fn build_blocks(
| "interface_declaration" | "interface_declaration"
| "type_alias_declaration" | "type_alias_declaration"
| "enum_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 { if let Some(name) = name_opt {
glue.retain(|(_, gs, _)| *gs < outer_s); glue.retain(|(_, gs, _)| *gs < outer_s);
flush_glue(glue, units, mod_prefix, mod_path); flush_glue(glue, units, mod_prefix, mod_path);
@@ -376,7 +376,7 @@ fn build_blocks(
| "class" | "class"
| "class_declaration" => { | "class_declaration" => {
let name_opt = 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 let leaf = name_opt
.as_deref() .as_deref()
.unwrap_or("default") .unwrap_or("default")
@@ -461,7 +461,7 @@ fn build_blocks(
// post-pass as 1A Gap 1 / Python). // post-pass as 1A Gap 1 / Python).
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real); let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
if has_real_unit { 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("<module>") { if !*is_real && sym.ends_with("<module>") {
let pre = &sym[..sym.len() - "<module>".len()]; let pre = &sym[..sym.len() - "<module>".len()];
*sym = format!("{pre}<top-level>"); *sym = format!("{pre}<top-level>");
@@ -481,7 +481,7 @@ fn build_blocks(
lang: Some("typescript".to_string()), lang: Some("typescript".to_string()),
}; };
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span); 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 { blocks.push(Block::Code(CodeBlock {
common: CommonBlock { common: CommonBlock {
block_id, block_id,

View File

@@ -55,3 +55,6 @@ base64 = { workspace = true }
# at runtime) is preserved. # at runtime) is preserved.
kebab-llm = { path = "../kebab-llm", features = ["mock"] } kebab-llm = { path = "../kebab-llm", features = ["mock"] }
kebab-llm-local = { path = "../kebab-llm-local" } kebab-llm-local = { path = "../kebab-llm-local" }
[lints]
workspace = true

View File

@@ -198,7 +198,7 @@ pub fn apply_caption(
/// language; everything else falls through to English. /// language; everything else falls through to English.
fn build_prompt(lang_hint: Option<&str>) -> (String, String) { fn build_prompt(lang_hint: Option<&str>) -> (String, String) {
match lang_hint { match lang_hint {
Some("ko") | Some("kor") => ( Some("ko" | "kor") => (
"이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \ "이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \
보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \ 보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \
한 문장만 출력." 한 문장만 출력."

View File

@@ -103,7 +103,7 @@ fn ascii_field(exif: &exif::Exif, tag: Tag) -> Option<String> {
fn u32_field(exif: &exif::Exif, tag: Tag) -> Option<u32> { fn u32_field(exif: &exif::Exif, tag: Tag) -> Option<u32> {
let f = exif.get_field(tag, In::PRIMARY)?; let f = exif.get_field(tag, In::PRIMARY)?;
match &f.value { 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(), Value::Long(v) => v.first().copied(),
_ => None, _ => None,
} }
@@ -177,7 +177,7 @@ fn rational_to_f64(r: &exif::Rational) -> Option<f64> {
if r.denom == 0 { if r.denom == 0 {
None None
} else { } else {
Some(r.num as f64 / r.denom as f64) Some(f64::from(r.num) / f64::from(r.denom))
} }
} }

View File

@@ -162,9 +162,7 @@ mod tests {
let ratio = w as f32 / h as f32; let ratio = w as f32 / h as f32;
assert!( assert!(
(ratio - 4.0 / 3.0).abs() < 0.02, (ratio - 4.0 / 3.0).abs() < 0.02,
"aspect drift: in=4/3 out={}/{}={ratio}", "aspect drift: in=4/3 out={w}/{h}={ratio}"
w,
h
); );
} }

View File

@@ -142,7 +142,7 @@ fn splice_exif_into_jpeg(exif_blob: Vec<u8>) -> Vec<u8> {
// + exif_blob.len(). Pre-validated against the 0xFFFF segment limit. // + exif_blob.len(). Pre-validated against the 0xFFFF segment limit.
let app1_payload_len = 2 + 6 + exif_blob.len(); let app1_payload_len = 2 + 6 + exif_blob.len();
assert!( assert!(
app1_payload_len <= u16::MAX as usize, u16::try_from(app1_payload_len).is_ok(),
"EXIF segment too large for a single APP1" "EXIF segment too large for a single APP1"
); );
out.extend_from_slice(&(app1_payload_len as u16).to_be_bytes()); out.extend_from_slice(&(app1_payload_len as u16).to_be_bytes());

View File

@@ -80,8 +80,8 @@ fn jpeg_with_exif_gps_captures_whitelisted_tags() {
Some(&Value::String("2024-08-15T12:34:56".into())) Some(&Value::String("2024-08-15T12:34:56".into()))
); );
assert_eq!(exif.get("orientation"), Some(&Value::Number(1.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 lat = exif.get("gps_lat").and_then(serde_json::Value::as_f64).expect("gps_lat");
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!((lat - 37.5).abs() < 1e-6, "lat={lat}"); assert!((lat - 37.5).abs() < 1e-6, "lat={lat}");
assert!((lon - 127.0).abs() < 1e-6, "lon={lon}"); 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"), !exif.contains_key("gps_lat"),
"out-of-range latitude must be dropped" "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); assert!((lon - 127.0).abs() < 1e-6);
} }

View File

@@ -388,7 +388,7 @@ async fn ocr_integration_real_ollama_transcribes_text() {
.expect("blocking task panicked") .expect("blocking task panicked")
.expect("real Ollama OCR must succeed"); .expect("real Ollama OCR must succeed");
eprintln!("integration OCR result: {:?}", text.joined); eprintln!("integration OCR result: {:?}", text.joined);
let normalized = text.joined.to_lowercase().replace(",", "").replace(".", ""); let normalized = text.joined.to_lowercase().replace(',', "").replace('.', "");
assert!( assert!(
normalized.contains("hello") && normalized.contains("world"), normalized.contains("hello") && normalized.contains("world"),
"integration OCR did not capture expected text: {:?}", "integration OCR did not capture expected text: {:?}",

View File

@@ -38,3 +38,6 @@ lingua = { version = "1.8", default-features = false, features = [
[dev-dependencies] [dev-dependencies]
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true

View File

@@ -60,18 +60,15 @@ pub fn parse_blocks(
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
parse_blocks_inner(body, body_offset_lines) parse_blocks_inner(body, body_offset_lines)
})); }));
match result { if let Ok(out) = result { Ok(out) } else {
Ok(out) => Ok(out), tracing::warn!("parse_blocks panicked on adversarial input; returning empty");
Err(_) => { Ok((
tracing::warn!("parse_blocks panicked on adversarial input; returning empty"); Vec::new(),
Ok(( vec![Warning {
Vec::new(), kind: WarningKind::ExtractFailed,
vec![Warning { note: "pulldown-cmark panicked; body discarded".to_string(),
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<ParsedBlock>,
// possibly-inverted spans would be more harmful than dropping output. // possibly-inverted spans would be more harmful than dropping output.
if state.overflow_detected { if state.overflow_detected {
let at = state let at = state
.overflow_at_body_line .overflow_at_body_line.map_or_else(|| "?".to_string(), |n| n.to_string());
.map(|n| n.to_string())
.unwrap_or_else(|| "?".to_string());
return ( return (
Vec::new(), Vec::new(),
vec![Warning { vec![Warning {
@@ -339,10 +334,10 @@ impl InlineBuf {
// `Inline::Link.text` field. Code/strong/emph inside a link are // `Inline::Link.text` field. Code/strong/emph inside a link are
// collapsed to their plain text — `Inline::Link` doesn't model // collapsed to their plain text — `Inline::Link` doesn't model
// formatting inside the link. // formatting inside the link.
let flat = if !text.is_empty() { let flat = if text.is_empty() {
text
} else {
flatten_inlines_to_text(&kids) flatten_inlines_to_text(&kids)
} else {
text
}; };
self.push_inline(Inline::Link { text: flat, href }); 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::Strong(kids) => self.push_inline(Inline::Strong { children: kids }),
InlineFrame::Emph(kids) => self.push_inline(Inline::Emph { children: kids }), InlineFrame::Emph(kids) => self.push_inline(Inline::Emph { children: kids }),
InlineFrame::Link { href, text, kids } => { InlineFrame::Link { href, text, kids } => {
let flat = if !text.is_empty() { let flat = if text.is_empty() {
text
} else {
flatten_inlines_to_text(&kids) flatten_inlines_to_text(&kids)
} else {
text
}; };
self.push_inline(Inline::Link { text: flat, href }); 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 // inverted span. Without this guard, debug builds panic with
// "attempt to add with overflow" (caught by `catch_unwind`, masking // "attempt to add with overflow" (caught by `catch_unwind`, masking
// the real cause) and release builds wrap to `start > end`. // 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), start_body.checked_add(self.body_offset_lines),
end_body.checked_add(self.body_offset_lines), end_body.checked_add(self.body_offset_lines),
) { ) { SourceSpan::Line { start, end } } else {
(Some(start), Some(end)) => SourceSpan::Line { start, end }, if !self.overflow_detected {
_ => { self.overflow_detected = true;
if !self.overflow_detected { self.overflow_at_body_line = Some(start_body);
self.overflow_detected = true; }
self.overflow_at_body_line = Some(start_body); SourceSpan::Line {
} start: start_body.saturating_add(self.body_offset_lines),
SourceSpan::Line { end: end_body.saturating_add(self.body_offset_lines),
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) => { Event::Start(Tag::Strong) => {
self.flag_non_image_in_paragraph(); 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) => { Event::Start(Tag::Emphasis) => {
self.flag_non_image_in_paragraph(); 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, .. }) => { Event::Start(Tag::Link { dest_url, .. }) => {
self.flag_non_image_in_paragraph(); self.flag_non_image_in_paragraph();
@@ -991,13 +983,13 @@ impl<'a> WalkState<'a> {
} }
} }
Event::End(TagEnd::Strong) => { Event::End(TagEnd::Strong) => {
self.with_current_inlines(|buf| buf.close_strong()); self.with_current_inlines(InlineBuf::close_strong);
} }
Event::End(TagEnd::Emphasis) => { Event::End(TagEnd::Emphasis) => {
self.with_current_inlines(|buf| buf.close_emph()); self.with_current_inlines(InlineBuf::close_emph);
} }
Event::End(TagEnd::Link) => { Event::End(TagEnd::Link) => {
self.with_current_inlines(|buf| buf.close_link()); self.with_current_inlines(InlineBuf::close_link);
} }
Event::End(TagEnd::Image) => { Event::End(TagEnd::Image) => {
if let Some(Frame::Paragraph { image_depth, .. }) = self.frames.last_mut() { if let Some(Frame::Paragraph { image_depth, .. }) = self.frames.last_mut() {
@@ -1480,8 +1472,7 @@ mod tests {
inl, inl,
Inline::Text { .. } | Inline::Code { .. } | Inline::Link { .. } | Inline::Strong { .. } | Inline::Emph { .. } Inline::Text { .. } | Inline::Code { .. } | Inline::Link { .. } | Inline::Strong { .. } | Inline::Emph { .. }
), ),
"unexpected inline kind: {:?}", "unexpected inline kind: {inl:?}"
inl
); );
} }
} }
@@ -1503,7 +1494,7 @@ mod tests {
// First item should contain "a" plus a flattened rendering // First item should contain "a" plus a flattened rendering
// of the nested sub-list. // of the nested sub-list.
let flat = flatten_inlines_to_text(&items[0]); 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("- x"), "first item missing '- x': {flat:?}");
assert!(flat.contains("- y"), "first item missing '- y': {flat:?}"); assert!(flat.contains("- y"), "first item missing '- y': {flat:?}");
let flat2 = flatten_inlines_to_text(&items[1]); let flat2 = flatten_inlines_to_text(&items[1]);

View File

@@ -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 body = &bytes[body_start..];
let metadata = derive_metadata(raw_opt, hints, body, &mut warnings); let metadata = derive_metadata(raw_opt, hints, body, &mut warnings);
@@ -430,30 +430,24 @@ fn derive_metadata(
// ---- source_type ---- // ---- source_type ----
let source_type = match raw.source_type.as_deref() { let source_type = match raw.source_type.as_deref() {
None => SourceType::Markdown, None => SourceType::Markdown,
Some(s) => match parse_source_type(s) { Some(s) => if let Some(st) = parse_source_type(s) { st } else {
Some(st) => st, warnings.push(Warning {
None => { kind: WarningKind::MalformedFrontmatter,
warnings.push(Warning { note: format!("unknown source_type={s}, defaulted to markdown"),
kind: WarningKind::MalformedFrontmatter, });
note: format!("unknown source_type={s}, defaulted to markdown"), SourceType::Markdown
});
SourceType::Markdown
}
}, },
}; };
// ---- trust_level ---- // ---- trust_level ----
let trust_level = match raw.trust_level.as_deref() { let trust_level = match raw.trust_level.as_deref() {
None => TrustLevel::Primary, None => TrustLevel::Primary,
Some(s) => match parse_trust_level(s) { Some(s) => if let Some(tl) = parse_trust_level(s) { tl } else {
Some(tl) => tl, warnings.push(Warning {
None => { kind: WarningKind::MalformedFrontmatter,
warnings.push(Warning { note: format!("unknown trust_level={s}, defaulted to primary"),
kind: WarningKind::MalformedFrontmatter, });
note: format!("unknown trust_level={s}, defaulted to primary"), TrustLevel::Primary
});
TrustLevel::Primary
}
}, },
}; };

View File

@@ -24,3 +24,6 @@ lopdf = "0.32"
[dev-dependencies] [dev-dependencies]
blake3 = { workspace = true } blake3 = { workspace = true }
[lints]
workspace = true

View File

@@ -111,7 +111,7 @@ impl Extractor for PdfTextExtractor {
}); });
let mut blocks: Vec<Block> = Vec::with_capacity(pages.len()); let mut blocks: Vec<Block> = 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) { let (text, warning) = match page_text::extract_one(&pdf_doc, page_num) {
Ok(t) if !t.trim().is_empty() => (t, None), Ok(t) if !t.trim().is_empty() => (t, None),
Ok(_) => ( Ok(_) => (

View File

@@ -10,3 +10,6 @@ description = "Parser intermediate representations (no parser libs allowed)"
[dependencies] [dependencies]
kebab-core = { path = "../kebab-core" } kebab-core = { path = "../kebab-core" }
serde = { workspace = true } serde = { workspace = true }
[lints]
workspace = true

View File

@@ -28,3 +28,6 @@ kebab-llm = { path = "../kebab-llm", features = ["mock"] }
tempfile = { workspace = true } tempfile = { workspace = true }
rusqlite = { workspace = true } rusqlite = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
[lints]
workspace = true

View File

@@ -318,7 +318,7 @@ impl RagPipeline {
}); });
} }
let chunks_returned = u32::try_from(hits.len()).unwrap_or(u32::MAX); 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!( tracing::debug!(
target: "kebab-rag", target: "kebab-rag",
@@ -856,7 +856,7 @@ impl RagPipeline {
}); });
} }
let chunks_returned = u32::try_from(pool.len()).unwrap_or(u32::MAX); 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 ────────────────────────────────────── // ── 3. Score gate / no chunks ──────────────────────────────────────
// PR-3b-ii: forward the partial hop trace into the refusal so // PR-3b-ii: forward the partial hop trace into the refusal so
@@ -1149,7 +1149,7 @@ impl RagPipeline {
refusal_phrase_detected = matched_refusal_phrase, refusal_phrase_detected = matched_refusal_phrase,
finish_reason = ?finish_reason, finish_reason = ?finish_reason,
chunks_used, 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" "kb-rag: multi-hop ask done"
); );
@@ -1388,16 +1388,13 @@ impl RagPipeline {
let chunk_full = let chunk_full =
<SqliteStore as kebab_core::DocumentStore>::get_chunk(&self.docs, &hit.chunk_id) <SqliteStore as kebab_core::DocumentStore>::get_chunk(&self.docs, &hit.chunk_id)
.context("kb-rag: docs.get_chunk")?; .context("kb-rag: docs.get_chunk")?;
let chunk_text = match chunk_full { let chunk_text = if let Some(c) = chunk_full { c.text } else {
Some(c) => c.text, tracing::warn!(
None => { target: "kebab-rag",
tracing::warn!( chunk_id = %hit.chunk_id.0,
target: "kebab-rag", "kb-rag: chunk not found in store; skipping"
chunk_id = %hit.chunk_id.0, );
"kb-rag: chunk not found in store; skipping" continue;
);
continue;
}
}; };
let header = format!( let header = format!(
"[#{n}] doc={} heading={} span={}\n", "[#{n}] doc={} heading={} span={}\n",
@@ -1999,13 +1996,11 @@ fn strip_markdown_json_fence(s: &str) -> &str {
let after_open = trimmed let after_open = trimmed
.strip_prefix("```json") .strip_prefix("```json")
.or_else(|| trimmed.strip_prefix("```")) .or_else(|| trimmed.strip_prefix("```"))
.map(|rest| rest.trim_start_matches('\n')) .map_or(trimmed, |rest| rest.trim_start_matches('\n'));
.unwrap_or(trimmed);
let inner = after_open let inner = after_open
.trim_end() .trim_end()
.strip_suffix("```") .strip_suffix("```")
.map(|rest| rest.trim_end()) .map_or(after_open, str::trim_end);
.unwrap_or(after_open);
inner.trim() inner.trim()
} }

View File

@@ -147,7 +147,7 @@ pub fn mk_hit_with_indexed_at(
chunk_id: ChunkId(chunk_id.to_string()), chunk_id: ChunkId(chunk_id.to_string()),
doc_id: DocumentId(doc_id.to_string()), doc_id: DocumentId(doc_id.to_string()),
doc_path: p.clone(), 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, section_label: None,
snippet: "snippet".to_string(), snippet: "snippet".to_string(),
citation: Citation::Line { citation: Citation::Line {

View File

@@ -68,7 +68,7 @@ fn multi_hop_decide_stop_triggers_synthesize() {
// Three LLM calls in order: decompose → decide → synthesize. // Three LLM calls in order: decompose → decide → synthesize.
let lm = Arc::new(ScriptedLm::new(vec![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1"]"#, r#"["q1"]"#,
r#"[]"#, r"[]",
"answer body [#1]", "answer body [#1]",
])); ]));
let lm_handle = lm.clone(); let lm_handle = lm.clone();
@@ -131,7 +131,7 @@ fn multi_hop_decide_continue_adds_more_chunks() {
let lm = Arc::new(ScriptedLm::new(vec![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1"]"#, r#"["q1"]"#,
r#"["q2"]"#, r#"["q2"]"#,
r#"[]"#, r"[]",
"synthesized [#1] [#2]", "synthesized [#1] [#2]",
])); ]));
let lm_handle = lm.clone(); 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![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1", "q2"]"#, r#"["q1", "q2"]"#,
r#"[]"#, r"[]",
"merged answer [#1]", "merged answer [#1]",
])); ]));
let lm_handle = lm.clone(); 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. // never runs because we refuse before pack_context.
let lm = Arc::new(ScriptedLm::new(vec![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1"]"#, r#"["q1"]"#,
r#"[]"#, r"[]",
])); ]));
let lm_handle = lm.clone(); let lm_handle = lm.clone();
let lm_dyn: Arc<dyn LanguageModel> = lm; let lm_dyn: Arc<dyn LanguageModel> = lm;
@@ -594,7 +594,7 @@ fn multi_hop_above_probe_gate_proceeds_to_decompose() {
let lm = Arc::new(ScriptedLm::new(vec![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1"]"#, r#"["q1"]"#,
r#"[]"#, r"[]",
"answer [#1]", "answer [#1]",
])); ]));
let lm_handle = lm.clone(); let lm_handle = lm.clone();
@@ -649,7 +649,7 @@ fn happy_multi_hop_env() -> (RagEnv, Arc<ScriptedRetriever>, Arc<ScriptedLm>) {
let retriever = Arc::new(ScriptedRetriever::new(vec![hits.clone(), hits])); let retriever = Arc::new(ScriptedRetriever::new(vec![hits.clone(), hits]));
let lm = Arc::new(ScriptedLm::new(vec![ let lm = Arc::new(ScriptedLm::new(vec![
r#"["q1"]"#, r#"["q1"]"#,
r#"[]"#, r"[]",
"answer body [#1]", "answer body [#1]",
])); ]));
(env, retriever, lm) (env, retriever, lm)

View File

@@ -282,8 +282,7 @@ fn streaming_forwards_tokens_to_sink() {
StreamEvent::Token { delta, .. } => Some(delta), StreamEvent::Token { delta, .. } => Some(delta),
_ => None, _ => None,
}) })
.collect::<Vec<_>>() .collect::<String>();
.join("");
assert_eq!(collected, canned); assert_eq!(collected, canned);
} }
@@ -522,7 +521,7 @@ fn answer_json_serializes_with_expected_keys() {
let answer = pipeline.ask("what", default_opts()).unwrap(); let answer = pipeline.ask("what", default_opts()).unwrap();
let v: serde_json::Value = serde_json::to_value(&answer).unwrap(); let v: serde_json::Value = serde_json::to_value(&answer).unwrap();
// Stable top-level key set per `answer.v1` (§2.3). // 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 [ for needed in [
"answer", "answer",
"citations", "citations",

View File

@@ -36,3 +36,6 @@ tempfile = { workspace = true }
# The mock-retriever unit tests (the bulk of the hybrid suite) do not # The mock-retriever unit tests (the bulk of the hybrid suite) do not
# need either, but the integration / snapshot lane does. # need either, but the integration / snapshot lane does.
kebab-embed = { path = "../kebab-embed", features = ["mock"] } kebab-embed = { path = "../kebab-embed", features = ["mock"] }
[lints]
workspace = true

View File

@@ -601,7 +601,7 @@ mod tests {
let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 5); let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 5);
let out = h.search(&make_query(SearchMode::Hybrid, 5)).unwrap(); let out = h.search(&make_query(SearchMode::Hybrid, 5)).unwrap();
let a = out.iter().find(|h| h.chunk_id.0 == "aaaa").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 // Tolerance: the score is computed in f64 and cast to f32 at
// the API boundary, so any discrepancy must fit within f32 // the API boundary, so any discrepancy must fit within f32
// precision. `1e-7` is below `f32::EPSILON` (~1.19e-7), which // 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 h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 4);
let out = h.search(&make_query(SearchMode::Hybrid, 4)).unwrap(); 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(); 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"]); assert_eq!(ids, vec!["aaaa", "bbbb", "cccc", "dddd"]);
} }

View File

@@ -457,7 +457,7 @@ fn run_query(
.prepare(&sql) .prepare(&sql)
.context("kb-search lexical: prepare FTS5 statement")?; .context("kb-search lexical: prepare FTS5 statement")?;
let rows = stmt 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")?; .context("kb-search lexical: execute FTS5 query")?;
let mut out: Vec<RawRow> = Vec::new(); let mut out: Vec<RawRow> = Vec::new();
for r in rows { for r in rows {

View File

@@ -37,12 +37,10 @@ use tempfile::TempDir;
pub fn require_avx_or_panic() { pub fn require_avx_or_panic() {
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
{ {
if !std::is_x86_feature_detected!("avx") { assert!(std::is_x86_feature_detected!("avx"),
panic!( "kb-search hybrid integration test requires AVX-capable hardware; \
"kb-search hybrid integration test requires AVX-capable hardware; \ host CPU lacks AVX. Run on an AVX-capable machine."
host CPU lacks AVX. Run on an AVX-capable machine." );
);
}
} }
} }
@@ -285,7 +283,7 @@ impl HybridEnv {
vector, vector,
doc_id: DocumentId(doc_id.to_string()), doc_id: DocumentId(doc_id.to_string()),
text: text.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_id: EmbeddingModelId(TEST_MODEL_ID.to_string()),
model_version: EmbeddingVersion("v1".to_string()), model_version: EmbeddingVersion("v1".to_string()),
dimensions: TEST_DIMENSIONS, dimensions: TEST_DIMENSIONS,

View File

@@ -186,14 +186,12 @@ fn hybrid_snapshot_run_1() {
// Refuse to silently "pass" against the committed placeholder. The // Refuse to silently "pass" against the committed placeholder. The
// placeholder JSON carries a `_comment` field with regeneration // placeholder JSON carries a `_comment` field with regeneration
// instructions; production fixtures (a captured list) do not. // instructions; production fixtures (a captured list) do not.
if expected.get("_comment").is_some() { assert!(!expected.get("_comment").is_some(),
panic!( "snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \
"snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \ Path: {}. To regenerate: \
Path: {}. To regenerate: \ `KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.",
`KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.", fixture.display()
fixture.display() );
);
}
assert_eq!( assert_eq!(
actual, expected, actual, expected,

View File

@@ -23,3 +23,6 @@ globset = "0.4"
[dev-dependencies] [dev-dependencies]
serde_json = { workspace = true } serde_json = { workspace = true }
tempfile = "3" tempfile = "3"
[lints]
workspace = true

View File

@@ -21,7 +21,7 @@ pub(crate) fn media_type_for(path: &Path) -> MediaType {
let ext = path let ext = path
.extension() .extension()
.and_then(|s| s.to_str()) .and_then(|s| s.to_str())
.map(|s| s.to_ascii_lowercase()) .map(str::to_ascii_lowercase)
.unwrap_or_default(); .unwrap_or_default();
match ext.as_str() { match ext.as_str() {

Some files were not shown because too many files have changed in this diff Show More