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:
88
Cargo.toml
88
Cargo.toml
@@ -34,6 +34,94 @@ license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/altair823/kebab"
|
||||
version = "0.17.2"
|
||||
|
||||
# pre-v0.18 workspace-wide cleanup: enable clippy::pedantic group with
|
||||
# intentional allow-list. The allowed lints are either cosmetic (doc style),
|
||||
# informational (function size), or carry intentional truncation we accept
|
||||
# (numeric casts in tokenizer/ONNX inputs, hash modular reduction, etc).
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
# Intentional u32 ↔ i64 casts in kebab-nli (ONNX i64 inputs from tokenizer u32 ids).
|
||||
# u64 ↔ usize across kebab-store-sqlite row counts. Wide truncation is auditable
|
||||
# at use site, not lint-wide.
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
# Doc markdown style is cosmetic; we run rustdoc on demand.
|
||||
doc_markdown = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
# Informational only — splitting a long pipeline function isn't always cleaner.
|
||||
too_many_lines = "allow"
|
||||
# `Foo::default()` is concise and idiomatic here; `<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]
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
|
||||
@@ -81,3 +81,6 @@ lopdf = "0.32"
|
||||
# error_wire::tests::llm_unreachable_classifies_to_model_unreachable needs a real
|
||||
# reqwest::Error (private constructor) — built from a connect-refused call.
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -293,7 +293,7 @@ impl App {
|
||||
// so other in-flight searches can use the cache concurrently.
|
||||
drop(guard);
|
||||
let hits = self.search_uncached(query)?;
|
||||
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
guard.put(key, hits.clone());
|
||||
Ok(hits)
|
||||
}
|
||||
@@ -467,7 +467,7 @@ impl App {
|
||||
|
||||
// Snippet truncation if opts.snippet_chars set (mirror non-trace path).
|
||||
if opts.snippet_chars.is_some() {
|
||||
for h in hits.iter_mut() {
|
||||
for h in &mut hits {
|
||||
if h.snippet.chars().count() > snippet_chars {
|
||||
h.snippet = trim_to_chars(&h.snippet, snippet_chars);
|
||||
}
|
||||
@@ -502,7 +502,7 @@ impl App {
|
||||
// `config.search.snippet_chars`; this only kicks in when the
|
||||
// caller asked for *less*).
|
||||
if opts.snippet_chars.is_some() {
|
||||
for h in hits.iter_mut() {
|
||||
for h in &mut hits {
|
||||
if h.snippet.chars().count() > snippet_chars {
|
||||
h.snippet = trim_to_chars(&h.snippet, snippet_chars);
|
||||
}
|
||||
@@ -521,7 +521,7 @@ impl App {
|
||||
{
|
||||
current_snippet_cap =
|
||||
(current_snippet_cap / 2).max(SNIPPET_FLOOR);
|
||||
for h in hits.iter_mut() {
|
||||
for h in &mut hits {
|
||||
if h.snippet.chars().count() > current_snippet_cap {
|
||||
h.snippet =
|
||||
trim_to_chars(&h.snippet, current_snippet_cap);
|
||||
@@ -868,7 +868,7 @@ impl App {
|
||||
/// clear` admin command). No-op when the cache is disabled.
|
||||
pub fn clear_search_cache(&self) {
|
||||
if let Some(cache) = self.search_cache.as_ref() {
|
||||
let mut guard = cache.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut guard = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
guard.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,9 +139,8 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> {
|
||||
|
||||
let k = obj
|
||||
.get("k")
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as usize)
|
||||
.unwrap_or(0); // 0 → use config default in app
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map_or(0, |n| n as usize); // 0 → use config default in app
|
||||
|
||||
let trust_min = match obj.get("trust_min").and_then(|v| v.as_str()) {
|
||||
None => None,
|
||||
@@ -209,14 +208,14 @@ fn parse_one(raw: &Value) -> Result<(SearchQuery, SearchOpts), String> {
|
||||
let opts = SearchOpts {
|
||||
max_tokens: obj
|
||||
.get("max_tokens")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|n| n as usize),
|
||||
snippet_chars: obj
|
||||
.get("snippet_chars")
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|n| n as usize),
|
||||
cursor: obj.get("cursor").and_then(|v| v.as_str()).map(String::from),
|
||||
trace: obj.get("trace").and_then(|v| v.as_bool()).unwrap_or(false),
|
||||
trace: obj.get("trace").and_then(serde_json::Value::as_bool).unwrap_or(false),
|
||||
};
|
||||
|
||||
Ok((
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn classify(err: &anyhow::Error, verbose: bool) -> ErrorV1 {
|
||||
}
|
||||
let mut details = json!({});
|
||||
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});
|
||||
}
|
||||
ErrorV1 {
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn ensure_kebabignore_entry(workspace_root: &Path) -> Result<()> {
|
||||
if !existing.is_empty() && !existing.ends_with('\n') {
|
||||
file.write_all(b"\n")?;
|
||||
}
|
||||
writeln!(file, "{}", KEBABIGNORE_LINE)?;
|
||||
writeln!(file, "{KEBABIGNORE_LINE}")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -166,8 +166,8 @@ mod tests {
|
||||
};
|
||||
let v = serde_json::to_value(&ev).unwrap();
|
||||
assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("asset_started"));
|
||||
assert_eq!(v.get("idx").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v.get("total").and_then(|n| n.as_u64()), Some(10));
|
||||
assert_eq!(v.get("idx").and_then(serde_json::Value::as_u64), Some(1));
|
||||
assert_eq!(v.get("total").and_then(serde_json::Value::as_u64), Some(10));
|
||||
assert_eq!(v.get("path").and_then(|s| s.as_str()), Some("notes/foo.md"));
|
||||
assert_eq!(v.get("media").and_then(|s| s.as_str()), Some("markdown"));
|
||||
}
|
||||
@@ -184,8 +184,8 @@ mod tests {
|
||||
let v = serde_json::to_value(&ev).unwrap();
|
||||
assert_eq!(v.get("kind").and_then(|s| s.as_str()), Some("completed"));
|
||||
let counts = v.get("counts").unwrap();
|
||||
assert_eq!(counts.get("scanned").and_then(|n| n.as_u64()), Some(5));
|
||||
assert_eq!(counts.get("new").and_then(|n| n.as_u64()), Some(2));
|
||||
assert_eq!(counts.get("scanned").and_then(serde_json::Value::as_u64), Some(5));
|
||||
assert_eq!(counts.get("new").and_then(serde_json::Value::as_u64), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -289,8 +289,7 @@ pub fn ingest_with_config_opts(
|
||||
let cancelled = || {
|
||||
opts.cancel
|
||||
.as_ref()
|
||||
.map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.unwrap_or(false)
|
||||
.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
|
||||
};
|
||||
let force_reingest = opts.force_reingest;
|
||||
let started_instant = std::time::Instant::now();
|
||||
@@ -394,7 +393,7 @@ pub fn ingest_with_config_opts(
|
||||
let purged_deleted_files = sweep_deleted_files(
|
||||
&app,
|
||||
&scanned_paths,
|
||||
vector_store.as_ref().map(|v| v.as_ref()),
|
||||
vector_store.as_ref().map(std::convert::AsRef::as_ref),
|
||||
)?;
|
||||
|
||||
let started_at = time::OffsetDateTime::now_utc();
|
||||
@@ -509,10 +508,10 @@ pub fn ingest_with_config_opts(
|
||||
*skipped_by_extension.entry(ext).or_insert(0) += 1;
|
||||
}
|
||||
kebab_core::IngestItemKind::Unchanged => {
|
||||
unchanged_count = unchanged_count.saturating_add(1)
|
||||
unchanged_count = unchanged_count.saturating_add(1);
|
||||
}
|
||||
kebab_core::IngestItemKind::Error => {
|
||||
error_count = error_count.saturating_add(1)
|
||||
error_count = error_count.saturating_add(1);
|
||||
}
|
||||
}
|
||||
crate::ingest_progress::emit(
|
||||
@@ -940,9 +939,7 @@ fn try_skip_unchanged(
|
||||
fn ext_for_skip_warning(path: &str) -> String {
|
||||
std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
.unwrap_or_else(|| NO_EXT_SENTINEL.to_string())
|
||||
.and_then(|s| s.to_str()).map_or_else(|| NO_EXT_SENTINEL.to_string(), str::to_ascii_lowercase)
|
||||
}
|
||||
|
||||
/// p9-fb-25: render the `IngestItem.warnings` line for a Skipped
|
||||
@@ -2407,7 +2404,7 @@ fn lang_hint_from_doc(doc: &CanonicalDocument) -> Option<Lang> {
|
||||
|
||||
/// Convenience: end byte of the frontmatter region (or 0 when absent).
|
||||
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
|
||||
@@ -2710,8 +2707,7 @@ pub fn ingest_file_with_config(
|
||||
const SUPPORTED_EXTS: &[&str] = &["md", "pdf", "png", "jpg", "jpeg"];
|
||||
if !SUPPORTED_EXTS.contains(&ext.as_str()) {
|
||||
anyhow::bail!(
|
||||
"ingest-file: unsupported extension `.{}` (supported: {:?})",
|
||||
ext, SUPPORTED_EXTS
|
||||
"ingest-file: unsupported extension `.{ext}` (supported: {SUPPORTED_EXTS:?})"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ fn collect_stats(
|
||||
store: &kebab_store_sqlite::SqliteStore,
|
||||
) -> anyhow::Result<Stats> {
|
||||
let counts = store
|
||||
.count_summary_with_threshold(cfg.search.stale_threshold_days as u64)?;
|
||||
.count_summary_with_threshold(u64::from(cfg.search.stale_threshold_days))?;
|
||||
let data_dir = kebab_config::expand_path(&cfg.storage.data_dir, "");
|
||||
let index_bytes = kebab_store_sqlite::stats_ext::index_bytes(&data_dir)
|
||||
.map_err(|e| anyhow::anyhow!("index_bytes: {e}"))?;
|
||||
|
||||
@@ -1267,7 +1267,7 @@ fn tier1_cpp_ingest_searchable() {
|
||||
// (method) depending on which chunk ranks first.
|
||||
assert!(
|
||||
symbol.as_deref().is_some_and(|s| s.starts_with("kebab::chunk::Foo")),
|
||||
"C++ symbol must start with namespace::Class prefix, got {:?}", symbol
|
||||
"C++ symbol must start with namespace::Class prefix, got {symbol:?}"
|
||||
);
|
||||
assert!(*line_start >= 1, "line_start must be >=1");
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fn ingest_file_copies_external_md_and_reports_new() {
|
||||
assert!(ext_dir.is_dir());
|
||||
let entries: Vec<_> = fs::read_dir(&ext_dir)
|
||||
.unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
assert_eq!(entries.len(), 1, "exactly one file in _external/");
|
||||
let name = entries[0].file_name().to_string_lossy().into_owned();
|
||||
|
||||
@@ -35,7 +35,7 @@ fn ingest_stdin_writes_frontmatter_and_reports_new() {
|
||||
// _external/ contains exactly one .md file with frontmatter.
|
||||
let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external");
|
||||
let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
assert_eq!(entries.len(), 1);
|
||||
let content = fs::read_to_string(entries[0].path()).unwrap();
|
||||
@@ -60,7 +60,7 @@ fn ingest_stdin_without_source_uri() {
|
||||
|
||||
let ext_dir = std::path::PathBuf::from(&cfg.workspace.root).join("_external");
|
||||
let entries: Vec<_> = fs::read_dir(&ext_dir).unwrap()
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(std::result::Result::ok)
|
||||
.collect();
|
||||
let content = fs::read_to_string(entries[0].path()).unwrap();
|
||||
assert!(content.contains("title: \"Title\""));
|
||||
|
||||
@@ -14,12 +14,10 @@ use common::TestEnv;
|
||||
fn require_avx_or_panic() {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
if !std::is_x86_feature_detected!("avx") {
|
||||
panic!(
|
||||
"kb-app vector integration test requires AVX-capable hardware; \
|
||||
host CPU lacks AVX. Run on an AVX-capable machine."
|
||||
);
|
||||
}
|
||||
assert!(std::is_x86_feature_detected!("avx"),
|
||||
"kb-app vector integration test requires AVX-capable hardware; \
|
||||
host CPU lacks AVX. Run on an AVX-capable machine."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,3 +26,6 @@ kebab-parse-code = { path = "../kebab-parse-code" }
|
||||
kebab-normalize = { path = "../kebab-normalize" }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -266,7 +266,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn oversize_unit_splits_into_parts_with_unique_ids() {
|
||||
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<Vec<_>>().join("");
|
||||
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<String>();
|
||||
let code = format!("int big() {{\n{body}\n}}");
|
||||
let doc = code_doc(&[("big", 1, 502, &code)]);
|
||||
let chunks = CodeCAstV1Chunker.chunk(&doc, &policy()).unwrap();
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn oversize_unit_splits_into_parts_with_unique_ids() {
|
||||
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<Vec<_>>().join("");
|
||||
let body = (0..500).map(|i| format!("\tx{i} = {i};\n")).collect::<String>();
|
||||
let code = format!("int big() {{\n{body}\n}}");
|
||||
let doc = code_doc(&[("big", 1, 502, &code)]);
|
||||
let chunks = CodeCppAstV1Chunker.chunk(&doc, &policy()).unwrap();
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
let n = ids.len(); ids.sort(); ids.dedup();
|
||||
let n = ids.len(); ids.sort_unstable(); ids.dedup();
|
||||
assert_eq!(ids.len(), n, "chunk_ids unique across split parts");
|
||||
}
|
||||
|
||||
|
||||
@@ -387,9 +387,7 @@ fn render_block_text(b: &Block) -> String {
|
||||
// alt keeps lexical search hits on filenames working even when
|
||||
// P6-1's filename auto-fill is bypassed.
|
||||
Block::ImageRef(i) => {
|
||||
let alt = if !i.alt.is_empty() {
|
||||
i.alt.clone()
|
||||
} else {
|
||||
let alt = if i.alt.is_empty() {
|
||||
// P6-1 falls back to filename so this branch is
|
||||
// defensive — keep it lest a future test fixture or
|
||||
// synthetic block path skip the auto-fill.
|
||||
@@ -399,17 +397,17 @@ fn render_block_text(b: &Block) -> String {
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("[image]")
|
||||
.to_string()
|
||||
} else {
|
||||
i.alt.clone()
|
||||
};
|
||||
let ocr = i
|
||||
.ocr
|
||||
.as_ref()
|
||||
.map(|o| o.joined.as_str())
|
||||
.unwrap_or("");
|
||||
.map_or("", |o| o.joined.as_str());
|
||||
let cap = i
|
||||
.caption
|
||||
.as_ref()
|
||||
.map(|c| c.text.as_str())
|
||||
.unwrap_or("");
|
||||
.map_or("", |c| c.text.as_str());
|
||||
[alt.as_str(), ocr, cap]
|
||||
.iter()
|
||||
.filter(|s| !s.is_empty())
|
||||
|
||||
@@ -450,7 +450,7 @@ mod tests {
|
||||
// chunk_ids stay distinct despite identical block_ids — the
|
||||
// per-chunk policy_hash variant is doing its job.
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
ids.sort();
|
||||
ids.sort_unstable();
|
||||
let total = ids.len();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), total, "all chunk_ids must be unique");
|
||||
@@ -668,7 +668,7 @@ mod tests {
|
||||
// chunk_ids stay distinct (the per-chunk hash variant keys off
|
||||
// char_start which is now strictly increasing).
|
||||
let mut ids: Vec<&str> = chunks.iter().map(|c| c.chunk_id.0.as_str()).collect();
|
||||
ids.sort();
|
||||
ids.sort_unstable();
|
||||
let total = ids.len();
|
||||
ids.dedup();
|
||||
assert_eq!(ids.len(), total, "chunk_ids must remain unique");
|
||||
|
||||
@@ -280,9 +280,7 @@ fn k8s_oversize_splits_into_line_windows_sharing_symbol() {
|
||||
assert_eq!(
|
||||
prev_end + 1,
|
||||
next_start,
|
||||
"line ranges must be contiguous: {} → {} (got gap or overlap)",
|
||||
prev_end,
|
||||
next_start
|
||||
"line ranges must be contiguous: {prev_end} → {next_start} (got gap or overlap)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ fn manifest_doc(lang: &str, manifest_text: &str) -> CanonicalDocument {
|
||||
doc_id,
|
||||
source_asset_id: aid,
|
||||
workspace_path: wp,
|
||||
title: format!("Manifest ({})", lang),
|
||||
title: format!("Manifest ({lang})"),
|
||||
lang: Lang("und".into()),
|
||||
blocks: vec![block],
|
||||
metadata: Metadata {
|
||||
|
||||
@@ -50,3 +50,6 @@ tempfile = { workspace = true }
|
||||
# to simulate stale docs. `time` is the formatter used by the helper.
|
||||
rusqlite = { workspace = true }
|
||||
time = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -797,7 +797,7 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
serde_json::to_string(&item.query)?,
|
||||
)?;
|
||||
if let Some(err) = &item.error {
|
||||
writeln!(stdout, "error: {}", err)?;
|
||||
writeln!(stdout, "error: {err}")?;
|
||||
} else if let Some(resp) = &item.response {
|
||||
writeln!(
|
||||
stdout,
|
||||
@@ -1171,15 +1171,13 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
let report = kebab_app::reset::execute(scope, &cfg)?;
|
||||
if cli.json {
|
||||
println!("{}", serde_json::to_string(&wire::wire_reset(&report))?);
|
||||
} else {
|
||||
if report.orphans_purged > 0 {
|
||||
println!("orphans purged: {}", report.orphans_purged);
|
||||
for p in &report.purged_paths {
|
||||
println!(" - {}", p.0);
|
||||
}
|
||||
} else {
|
||||
println!("no orphaned docs found — store is already in sync with walker scope");
|
||||
} else if report.orphans_purged > 0 {
|
||||
println!("orphans purged: {}", report.orphans_purged);
|
||||
for p in &report.purged_paths {
|
||||
println!(" - {}", p.0);
|
||||
}
|
||||
} else {
|
||||
println!("no orphaned docs found — store is already in sync with walker scope");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
@@ -1508,11 +1506,11 @@ fn confirm_destructive(
|
||||
) -> anyhow::Result<bool> {
|
||||
use std::io::Write;
|
||||
let mut out = std::io::stderr().lock();
|
||||
writeln!(out, "kebab reset ({:?}): about to remove", scope)?;
|
||||
writeln!(out, "kebab reset ({scope:?}): about to remove")?;
|
||||
for p in paths {
|
||||
writeln!(out, " - {}", p.display())?;
|
||||
}
|
||||
writeln!(out, "estimated total: {} bytes", bytes)?;
|
||||
writeln!(out, "estimated total: {bytes} bytes")?;
|
||||
write!(out, "Proceed? [y/N] ")?;
|
||||
out.flush()?;
|
||||
|
||||
@@ -1573,19 +1571,19 @@ fn render_fetch_plain(r: &kebab_core::FetchResult) {
|
||||
if !r.context_before.is_empty() {
|
||||
println!("\n=== before ===");
|
||||
for c in &r.context_before {
|
||||
let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or("");
|
||||
let heading = c.heading_path.last().map_or("", std::string::String::as_str);
|
||||
println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
|
||||
}
|
||||
}
|
||||
if let Some(c) = &r.chunk {
|
||||
println!("\n=== target ===");
|
||||
let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or("");
|
||||
let heading = c.heading_path.last().map_or("", std::string::String::as_str);
|
||||
println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
|
||||
}
|
||||
if !r.context_after.is_empty() {
|
||||
println!("\n=== after ===");
|
||||
for c in &r.context_after {
|
||||
let heading = c.heading_path.last().map(|s| s.as_str()).unwrap_or("");
|
||||
let heading = c.heading_path.last().map_or("", std::string::String::as_str);
|
||||
println!("[{} § {}]\n{}\n", c.chunk_id.0, heading, c.text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ mod tests {
|
||||
v.get("next_cursor").and_then(|c| c.as_str()),
|
||||
Some("opaque-cursor-abc")
|
||||
);
|
||||
assert_eq!(v.get("truncated").and_then(|t| t.as_bool()), Some(true));
|
||||
assert_eq!(v.get("truncated").and_then(serde_json::Value::as_bool), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -88,5 +88,5 @@ max_context_tokens = 8000
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
|
||||
assert_eq!(v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1"));
|
||||
assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1));
|
||||
}
|
||||
|
||||
@@ -96,5 +96,5 @@ max_context_tokens = 8000
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
|
||||
assert_eq!(v.get("schema_version").and_then(|s| s.as_str()), Some("ingest_report.v1"));
|
||||
assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1));
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ fn cli_mcp_initialize_then_tools_list() {
|
||||
reader.read_line(&mut line).unwrap();
|
||||
let init: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
|
||||
assert_eq!(
|
||||
init.get("id").and_then(|i| i.as_i64()),
|
||||
init.get("id").and_then(serde_json::Value::as_i64),
|
||||
Some(1),
|
||||
"unexpected id in initialize response: {init}"
|
||||
);
|
||||
@@ -57,7 +57,7 @@ fn cli_mcp_initialize_then_tools_list() {
|
||||
reader.read_line(&mut line).unwrap();
|
||||
let list: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
|
||||
assert_eq!(
|
||||
list.get("id").and_then(|i| i.as_i64()),
|
||||
list.get("id").and_then(serde_json::Value::as_i64),
|
||||
Some(2),
|
||||
"unexpected id in tools/list response: {list}"
|
||||
);
|
||||
|
||||
@@ -76,8 +76,7 @@ fn cli_schema_json_emits_schema_v1() {
|
||||
assert!(
|
||||
v.get("kebab_version")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false),
|
||||
.is_some_and(|s| !s.is_empty()),
|
||||
"kebab_version must be a non-empty string"
|
||||
);
|
||||
|
||||
@@ -86,12 +85,12 @@ fn cli_schema_json_emits_schema_v1() {
|
||||
.and_then(|c| c.as_object())
|
||||
.expect("capabilities must be a JSON object");
|
||||
assert_eq!(
|
||||
caps.get("json_mode").and_then(|b| b.as_bool()),
|
||||
caps.get("json_mode").and_then(serde_json::Value::as_bool),
|
||||
Some(true),
|
||||
"capabilities.json_mode must be true"
|
||||
);
|
||||
assert_eq!(
|
||||
caps.get("mcp_server").and_then(|b| b.as_bool()),
|
||||
caps.get("mcp_server").and_then(serde_json::Value::as_bool),
|
||||
Some(true),
|
||||
"capabilities.mcp_server must be true (fb-30)"
|
||||
);
|
||||
|
||||
@@ -155,8 +155,8 @@ fn ingest_json_progress_lines_carry_kind_and_ts() {
|
||||
saw_completed = true;
|
||||
// Counts mirror the report.
|
||||
let counts = v.get("counts").unwrap();
|
||||
assert_eq!(counts.get("scanned").and_then(|n| n.as_u64()), Some(2));
|
||||
assert_eq!(counts.get("new").and_then(|n| n.as_u64()), Some(2));
|
||||
assert_eq!(counts.get("scanned").and_then(serde_json::Value::as_u64), Some(2));
|
||||
assert_eq!(counts.get("new").and_then(serde_json::Value::as_u64), Some(2));
|
||||
}
|
||||
}
|
||||
assert!(saw_scan_started, "missing scan_started event");
|
||||
|
||||
@@ -22,3 +22,6 @@ tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -157,7 +157,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn xdg_data_home_set_replaces_var() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _guard = XdgGuard::capture();
|
||||
// SAFETY: lock held for the duration of this test.
|
||||
unsafe { std::env::set_var("XDG_DATA_HOME", "/custom/path") };
|
||||
@@ -168,7 +168,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn xdg_data_home_unset_uses_default() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _guard = XdgGuard::capture();
|
||||
// SAFETY: lock held for the duration of this test.
|
||||
unsafe { std::env::remove_var("XDG_DATA_HOME") };
|
||||
@@ -181,7 +181,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn xdg_with_no_default_resolves_to_empty_when_unset() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _guard = XdgGuard::capture();
|
||||
// SAFETY: lock held for the duration of this test.
|
||||
unsafe { std::env::remove_var("XDG_DATA_HOME") };
|
||||
@@ -193,7 +193,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn leading_tilde_expands_to_home() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let home = std::env::var("HOME").expect("HOME must be set in tests");
|
||||
let p = expand_path("~/runs", "");
|
||||
assert_eq!(p, PathBuf::from(home).join("runs"));
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tilde_path_ignores_base_dir() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let home = std::env::var("HOME").expect("HOME must be set in tests");
|
||||
let base = Path::new("/tmp/ignored-cfg");
|
||||
let p = expand_path_with_base("~/x", "", base);
|
||||
@@ -238,7 +238,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn xdg_var_path_ignores_base_dir() {
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _guard = XdgGuard::capture();
|
||||
// SAFETY: lock held for the duration of this test.
|
||||
unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") };
|
||||
@@ -255,7 +255,7 @@ mod tests {
|
||||
// Order matters: substitute `{data_dir}` (which itself contains
|
||||
// an unexpanded `${XDG_DATA_HOME}` and `~`), then the other two
|
||||
// resolve the result.
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
||||
let _lock = ENV_LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let _guard = XdgGuard::capture();
|
||||
// SAFETY: lock held for the duration of this test.
|
||||
unsafe { std::env::set_var("XDG_DATA_HOME", "/xdg/data") };
|
||||
|
||||
@@ -16,3 +16,6 @@ time = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
serde_json_canonicalizer = "0.3"
|
||||
unicode-normalization = "0.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -226,28 +226,25 @@ fn parse_hms_ms(s: &str) -> Result<u64> {
|
||||
let m: u64 = parts[1]
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad minutes in {:?} (input {s:?})", parts[1]))?;
|
||||
let (sec, ms) = match parts[2].split_once('.') {
|
||||
Some((s_part, ms_part)) => {
|
||||
let sec: u64 = s_part
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?;
|
||||
// Pad/truncate to exactly 3 digits.
|
||||
let mut ms_str = ms_part.to_owned();
|
||||
while ms_str.len() < 3 {
|
||||
ms_str.push('0');
|
||||
}
|
||||
ms_str.truncate(3);
|
||||
let ms: u64 = ms_str
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?;
|
||||
(sec, ms)
|
||||
}
|
||||
None => {
|
||||
let sec: u64 = parts[2]
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?;
|
||||
(sec, 0)
|
||||
let (sec, ms) = if let Some((s_part, ms_part)) = parts[2].split_once('.') {
|
||||
let sec: u64 = s_part
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad seconds in {s_part:?} (input {s:?})"))?;
|
||||
// Pad/truncate to exactly 3 digits.
|
||||
let mut ms_str = ms_part.to_owned();
|
||||
while ms_str.len() < 3 {
|
||||
ms_str.push('0');
|
||||
}
|
||||
ms_str.truncate(3);
|
||||
let ms: u64 = ms_str
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?;
|
||||
(sec, ms)
|
||||
} else {
|
||||
let sec: u64 = parts[2]
|
||||
.parse()
|
||||
.map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?;
|
||||
(sec, 0)
|
||||
};
|
||||
Ok(h * 3_600_000 + m * 60_000 + sec * 1000 + ms)
|
||||
}
|
||||
|
||||
@@ -471,7 +471,7 @@ mod tests {
|
||||
doc_path: WorkspacePath("a.md".into()),
|
||||
heading_path: vec![],
|
||||
section_label: None,
|
||||
snippet: "".into(),
|
||||
snippet: String::new(),
|
||||
citation: Citation::Line {
|
||||
path: WorkspacePath("a.md".into()),
|
||||
start: 1,
|
||||
@@ -502,7 +502,7 @@ mod tests {
|
||||
doc_path: WorkspacePath("a.rs".into()),
|
||||
heading_path: vec![],
|
||||
section_label: None,
|
||||
snippet: "".into(),
|
||||
snippet: String::new(),
|
||||
citation: Citation::Code {
|
||||
path: WorkspacePath("a.rs".into()),
|
||||
line_start: 1,
|
||||
|
||||
@@ -20,3 +20,6 @@ anyhow = { workspace = true }
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -158,7 +158,7 @@ impl Embedder for FastembedEmbedder {
|
||||
let guard = self
|
||||
.inner
|
||||
.lock()
|
||||
.unwrap_or_else(|p| p.into_inner());
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let batch: Vec<Vec<f32>> = guard
|
||||
.embed(chunk_vec, Some(self.batch_size))
|
||||
.context("fastembed: embed")?;
|
||||
|
||||
@@ -28,3 +28,6 @@ mock = []
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -59,7 +59,7 @@ pub fn assert_vector_shape(vecs: &[Vec<f32>], expected_dims: usize) {
|
||||
/// Panics on mismatch (test-only helper — callers are tests).
|
||||
pub fn assert_unit_norm(vecs: &[Vec<f32>], tolerance: f32) {
|
||||
for (i, v) in vecs.iter().enumerate() {
|
||||
let norm_sq: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum();
|
||||
let norm_sq: f64 = v.iter().map(|&x| f64::from(x) * f64::from(x)).sum();
|
||||
let norm = norm_sq.sqrt() as f32;
|
||||
assert!(
|
||||
(norm - 1.0).abs() <= tolerance,
|
||||
|
||||
@@ -132,10 +132,10 @@ impl Embedder for MockEmbedder {
|
||||
.collect();
|
||||
|
||||
// L2-normalize. Skip the rare all-zero case to avoid 0/0 = NaN.
|
||||
let norm_sq: f64 = v.iter().map(|&x| (x as f64) * (x as f64)).sum();
|
||||
let norm_sq: f64 = v.iter().map(|&x| f64::from(x) * f64::from(x)).sum();
|
||||
if norm_sq > 0.0 {
|
||||
let inv = (1.0 / norm_sq.sqrt()) as f32;
|
||||
for x in v.iter_mut() {
|
||||
for x in &mut v {
|
||||
*x *= inv;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,6 @@ uuid = { workspace = true }
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -260,8 +260,8 @@ pub fn render_report_md(report: &CompareReport) -> String {
|
||||
"| {} | {} | {} | {} | {} |",
|
||||
c.query_id,
|
||||
comparison_kind_label(c.kind),
|
||||
c.a_hit_rank.map(|r| r.to_string()).unwrap_or_else(|| "—".into()),
|
||||
c.b_hit_rank.map(|r| r.to_string()).unwrap_or_else(|| "—".into()),
|
||||
c.a_hit_rank.map_or_else(|| "—".into(), |r| r.to_string()),
|
||||
c.b_hit_rank.map_or_else(|| "—".into(), |r| r.to_string()),
|
||||
c.note.as_deref().unwrap_or(""),
|
||||
);
|
||||
}
|
||||
@@ -308,7 +308,7 @@ fn extract_chunker_version(snapshot_json: &str) -> Option<String> {
|
||||
let v: serde_json::Value = serde_json::from_str(snapshot_json).ok()?;
|
||||
v.get("chunker_version")
|
||||
.and_then(|x| x.as_str())
|
||||
.map(|s| s.to_owned())
|
||||
.map(std::borrow::ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn parse_results(
|
||||
@@ -402,8 +402,7 @@ fn classify(
|
||||
// so refusal-flow queries (no expected_*) don't appear as
|
||||
// regressions.
|
||||
let has_expected = gq
|
||||
.map(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty())
|
||||
.unwrap_or(false);
|
||||
.is_some_and(|g| !g.expected_chunk_ids.is_empty() || !g.expected_doc_ids.is_empty());
|
||||
if has_expected {
|
||||
(ComparisonKind::Regression, Some("hit→miss".into()))
|
||||
} else {
|
||||
@@ -426,7 +425,7 @@ fn build_deltas(
|
||||
if a.is_nan() || b.is_nan() {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
serde_json::Value::from((b - a) as f64)
|
||||
serde_json::Value::from(f64::from(b - a))
|
||||
}
|
||||
}
|
||||
let mut hit = serde_json::Map::new();
|
||||
|
||||
@@ -270,7 +270,21 @@ pub(crate) fn aggregate_from_rows(
|
||||
// recall@k_doc (doc-level, requires non-empty expected_doc_ids
|
||||
// and `>0` is the "should retrieve" condition; refusal queries
|
||||
// (`expected_doc_ids = []`) are excluded by spec).
|
||||
if !gq.expected_doc_ids.is_empty() {
|
||||
if gq.expected_doc_ids.is_empty() {
|
||||
// refusal_correctness: golden marks "should refuse" via empty
|
||||
// expected_doc_ids. We can only judge this on RAG runs — a
|
||||
// lexical-only run produces no Answer, so "refusal" is
|
||||
// undefined. Excluding such queries from the denominator
|
||||
// (rather than counting them as failures) keeps the metric
|
||||
// honest: a search-only run reports refusal_correctness as
|
||||
// NaN/null, not 0.0.
|
||||
if let Some(ans) = &qr.answer {
|
||||
refusal_denom += 1;
|
||||
if !ans.grounded {
|
||||
refusal_num += 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let expected_docs: HashSet<&DocumentId> = gq.expected_doc_ids.iter().collect();
|
||||
for k in TOP_K_VARIANTS {
|
||||
let entry = recall_at_k_doc.get_mut(k).expect("init");
|
||||
@@ -285,20 +299,6 @@ pub(crate) fn aggregate_from_rows(
|
||||
let frac = covered as f64 / expected_docs.len() as f64;
|
||||
entry.0 += frac;
|
||||
}
|
||||
} else {
|
||||
// refusal_correctness: golden marks "should refuse" via empty
|
||||
// expected_doc_ids. We can only judge this on RAG runs — a
|
||||
// lexical-only run produces no Answer, so "refusal" is
|
||||
// undefined. Excluding such queries from the denominator
|
||||
// (rather than counting them as failures) keeps the metric
|
||||
// honest: a search-only run reports refusal_correctness as
|
||||
// NaN/null, not 0.0.
|
||||
if let Some(ans) = &qr.answer {
|
||||
refusal_denom += 1;
|
||||
if !ans.grounded {
|
||||
refusal_num += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// groundedness + citation_coverage (only meaningful with RAG
|
||||
|
||||
@@ -143,7 +143,7 @@ fn env_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
static M: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
M.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -147,7 +147,7 @@ fn lexical_opts() -> EvalRunOpts {
|
||||
/// guard must outlive the call so concurrent tests don't reset the
|
||||
/// var mid-run.
|
||||
fn run_with_golden<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
|
||||
// serializes mutations so concurrent tests don't race.
|
||||
unsafe {
|
||||
|
||||
@@ -34,3 +34,6 @@ anyhow = { workspace = true }
|
||||
# `tokio::*` symbols, so the public/runtime API stays sync.
|
||||
wiremock = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -400,9 +400,9 @@ impl Iterator for OllamaStream {
|
||||
// u32 saturation: even ~4G tokens is implausible for a
|
||||
// single chat turn; we still saturate rather than
|
||||
// panic on the unlikely case.
|
||||
prompt_tokens: prompt_tokens.min(u32::MAX as u64) as u32,
|
||||
completion_tokens: completion_tokens.min(u32::MAX as u64) as u32,
|
||||
latency_ms: (total_duration_ns / 1_000_000).min(u32::MAX as u64) as u32,
|
||||
prompt_tokens: prompt_tokens.min(u64::from(u32::MAX)) as u32,
|
||||
completion_tokens: completion_tokens.min(u64::from(u32::MAX)) as u32,
|
||||
latency_ms: (total_duration_ns / 1_000_000).min(u64::from(u32::MAX)) as u32,
|
||||
};
|
||||
return Some(Ok(TokenChunk::Done {
|
||||
finish_reason,
|
||||
|
||||
@@ -19,3 +19,6 @@ mock = []
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -27,3 +27,6 @@ kebab-core = { path = "../kebab-core" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -65,8 +65,7 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() {
|
||||
// Empty KB → refusal (grounded:false) is normal — NOT isError.
|
||||
assert!(
|
||||
!result.is_error.unwrap_or(false),
|
||||
"expected isError=false on refusal, got {:?}",
|
||||
result
|
||||
"expected isError=false on refusal, got {result:?}"
|
||||
);
|
||||
|
||||
let content = result
|
||||
@@ -86,7 +85,7 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() {
|
||||
"response should carry schema_version=answer.v1"
|
||||
);
|
||||
assert_eq!(
|
||||
v.get("grounded").and_then(|b| b.as_bool()),
|
||||
v.get("grounded").and_then(serde_json::Value::as_bool),
|
||||
Some(false),
|
||||
"empty KB should produce grounded=false"
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ async fn doctor_tool_returns_doctor_v1_json() {
|
||||
// `ok` boolean must be present (value may be false in CI where Ollama
|
||||
// is not reachable — that's expected and acceptable).
|
||||
assert!(
|
||||
v.get("ok").and_then(|b| b.as_bool()).is_some(),
|
||||
v.get("ok").and_then(serde_json::Value::as_bool).is_some(),
|
||||
"`ok` field missing in doctor.v1 response: {v}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,8 +98,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() {
|
||||
|
||||
assert!(
|
||||
!result.is_error.unwrap_or(false),
|
||||
"expected isError=false, got {:?}",
|
||||
result
|
||||
"expected isError=false, got {result:?}"
|
||||
);
|
||||
|
||||
let content = result
|
||||
@@ -123,7 +122,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() {
|
||||
"kind must be 'chunk'"
|
||||
);
|
||||
assert!(
|
||||
v.get("chunk").is_some_and(|c| c.is_object()),
|
||||
v.get("chunk").is_some_and(serde_json::Value::is_object),
|
||||
"chunk payload must be populated for kind=chunk"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ async fn ingest_file_tool_returns_ingest_report_v1() {
|
||||
v.get("schema_version").and_then(|s| s.as_str()),
|
||||
Some("ingest_report.v1")
|
||||
);
|
||||
assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -91,7 +91,7 @@ async fn ingest_file_tool_idempotent_on_second_call() {
|
||||
other => panic!("expected text, got {other:?}"),
|
||||
};
|
||||
let v1: serde_json::Value = serde_json::from_str(text1).unwrap();
|
||||
assert_eq!(v1.get("new").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v1.get("new").and_then(serde_json::Value::as_u64), Some(1));
|
||||
|
||||
// Second call — same content, expect unchanged=1.
|
||||
let r2 = tokio::task::spawn_blocking({
|
||||
@@ -112,6 +112,6 @@ async fn ingest_file_tool_idempotent_on_second_call() {
|
||||
other => panic!("expected text, got {other:?}"),
|
||||
};
|
||||
let v2: serde_json::Value = serde_json::from_str(text2).unwrap();
|
||||
assert_eq!(v2.get("new").and_then(|n| n.as_u64()), Some(0), "{v2:?}");
|
||||
assert_eq!(v2.get("unchanged").and_then(|n| n.as_u64()), Some(1), "{v2:?}");
|
||||
assert_eq!(v2.get("new").and_then(serde_json::Value::as_u64), Some(0), "{v2:?}");
|
||||
assert_eq!(v2.get("unchanged").and_then(serde_json::Value::as_u64), Some(1), "{v2:?}");
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ async fn ingest_stdin_tool_returns_ingest_report_v1() {
|
||||
v.get("schema_version").and_then(|s| s.as_str()),
|
||||
Some("ingest_report.v1")
|
||||
);
|
||||
assert_eq!(v.get("new").and_then(|n| n.as_u64()), Some(1));
|
||||
assert_eq!(v.get("new").and_then(serde_json::Value::as_u64), Some(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -49,8 +49,7 @@ async fn schema_tool_returns_schema_v1_json() {
|
||||
|
||||
assert!(
|
||||
!result.is_error.unwrap_or(false),
|
||||
"expected isError=false on healthy schema, got {:?}",
|
||||
result
|
||||
"expected isError=false on healthy schema, got {result:?}"
|
||||
);
|
||||
|
||||
let content = result.content.first().expect("expected at least one content item");
|
||||
@@ -68,7 +67,7 @@ async fn schema_tool_returns_schema_v1_json() {
|
||||
"unexpected schema_version in: {v}"
|
||||
);
|
||||
assert_eq!(
|
||||
v.get("capabilities").and_then(|c| c.get("mcp_server")).and_then(|b| b.as_bool()),
|
||||
v.get("capabilities").and_then(|c| c.get("mcp_server")).and_then(serde_json::Value::as_bool),
|
||||
Some(true),
|
||||
"mcp_server capability flag should be true after fb-30",
|
||||
);
|
||||
|
||||
@@ -71,8 +71,7 @@ async fn search_tool_returns_search_response_v1() {
|
||||
|
||||
assert!(
|
||||
!result.is_error.unwrap_or(false),
|
||||
"expected isError=false, got {:?}",
|
||||
result
|
||||
"expected isError=false, got {result:?}"
|
||||
);
|
||||
|
||||
let content = result
|
||||
@@ -108,7 +107,7 @@ async fn search_tool_returns_search_response_v1() {
|
||||
);
|
||||
// truncated must be present (bool); next_cursor may be null on last page.
|
||||
assert!(
|
||||
v.get("truncated").and_then(|t| t.as_bool()).is_some(),
|
||||
v.get("truncated").and_then(serde_json::Value::as_bool).is_some(),
|
||||
"envelope should carry truncated:bool"
|
||||
);
|
||||
assert!(
|
||||
@@ -172,8 +171,7 @@ async fn search_with_doc_id_filter_returns_only_target() {
|
||||
);
|
||||
assert!(
|
||||
!unfiltered.is_error.unwrap_or(false),
|
||||
"unfiltered search failed: {:?}",
|
||||
unfiltered
|
||||
"unfiltered search failed: {unfiltered:?}"
|
||||
);
|
||||
let unfiltered_text = match &unfiltered.content.first().unwrap().raw {
|
||||
RawContent::Text(t) => t.text.clone(),
|
||||
@@ -211,8 +209,7 @@ async fn search_with_doc_id_filter_returns_only_target() {
|
||||
);
|
||||
assert!(
|
||||
!filtered.is_error.unwrap_or(false),
|
||||
"filtered search failed: {:?}",
|
||||
filtered
|
||||
"filtered search failed: {filtered:?}"
|
||||
);
|
||||
let filtered_text = match &filtered.content.first().unwrap().raw {
|
||||
RawContent::Text(t) => t.text.clone(),
|
||||
|
||||
@@ -28,3 +28,6 @@ tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -235,11 +235,11 @@ impl NliVerifier for OnnxNliVerifier {
|
||||
.encode((premise, hypothesis), true)
|
||||
.map_err(|e| anyhow!("kebab-nli: tokenizer.encode failed: {e}"))?;
|
||||
|
||||
let ids: Vec<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
|
||||
.get_attention_mask()
|
||||
.iter()
|
||||
.map(|&u| u as i64)
|
||||
.map(|&u| i64::from(u))
|
||||
.collect();
|
||||
let seq_len = ids.len();
|
||||
|
||||
@@ -266,8 +266,7 @@ impl NliVerifier for OnnxNliVerifier {
|
||||
let shape = logits.shape();
|
||||
if shape != [1, LOGITS_LEN] {
|
||||
anyhow::bail!(
|
||||
"kebab-nli: unexpected logits shape {:?}, expected [1, {LOGITS_LEN}]",
|
||||
shape
|
||||
"kebab-nli: unexpected logits shape {shape:?}, expected [1, {LOGITS_LEN}]"
|
||||
);
|
||||
}
|
||||
let l = [logits[[0, 0]], logits[[0, 1]], logits[[0, 2]]];
|
||||
|
||||
@@ -111,8 +111,7 @@ fn long_premise_truncates_without_panic() {
|
||||
] {
|
||||
assert!(
|
||||
x.is_finite(),
|
||||
"channel {name} non-finite: {x} (full scores: {:?})",
|
||||
s
|
||||
"channel {name} non-finite: {x} (full scores: {s:?})"
|
||||
);
|
||||
}
|
||||
// Softmax invariant — the three channels sum to ~1.
|
||||
|
||||
@@ -25,3 +25,6 @@ tracing = { workspace = true }
|
||||
# default scope, excluding dev-deps) confirms this.
|
||||
kebab-parse-md = { path = "../kebab-parse-md" }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -27,3 +27,6 @@ tree-sitter-cpp = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -310,7 +310,7 @@ fn build_blocks(
|
||||
// If there is only glue (no real unit) the single pushed "<top-level>"
|
||||
// label should be "<module>" — rename it now.
|
||||
if !has_real_unit {
|
||||
for (sym, _, _, _) in units.iter_mut() {
|
||||
for (sym, _, _, _) in &mut units {
|
||||
if sym == "<top-level>" {
|
||||
*sym = "<module>".to_string();
|
||||
}
|
||||
@@ -329,7 +329,7 @@ fn build_blocks(
|
||||
lang: Some("c".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
@@ -704,11 +704,11 @@ void print_result(int v) {
|
||||
|
||||
#[test]
|
||||
fn c_extractor_deterministic_across_runs() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
struct Node { int val; };
|
||||
int sum(int a, int b) { return a + b; }
|
||||
void noop(void) {}
|
||||
"#;
|
||||
";
|
||||
let a = tests_support::extract_c(src, "x/det.c");
|
||||
for _ in 0..20 {
|
||||
assert_eq!(
|
||||
|
||||
@@ -224,7 +224,7 @@ fn build_blocks_top(
|
||||
units.push(("<module>".to_string(), 1, total.max(1), false));
|
||||
}
|
||||
if !has_real_unit {
|
||||
for (sym, _, _, _) in units.iter_mut() {
|
||||
for (sym, _, _, _) in &mut units {
|
||||
if sym == "<top-level>" {
|
||||
*sym = "<module>".to_string();
|
||||
}
|
||||
@@ -243,7 +243,7 @@ fn build_blocks_top(
|
||||
lang: Some("cpp".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
@@ -696,7 +696,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn namespace_and_class() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
namespace ns {
|
||||
class Foo {
|
||||
public:
|
||||
@@ -706,7 +706,7 @@ namespace ns {
|
||||
int operator+(const Foo& o) { return 0; }
|
||||
};
|
||||
}
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(s.iter().any(|x| x == "ns::Foo"), "ns::Foo missing: {s:?}");
|
||||
@@ -718,11 +718,11 @@ namespace ns {
|
||||
|
||||
#[test]
|
||||
fn anonymous_namespace() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
namespace {
|
||||
void hidden_fn() {}
|
||||
}
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(
|
||||
@@ -733,11 +733,11 @@ namespace {
|
||||
|
||||
#[test]
|
||||
fn nested_namespace_specifier() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
namespace outer::inner {
|
||||
void fn_in_nested() {}
|
||||
}
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(
|
||||
@@ -748,9 +748,9 @@ namespace outer::inner {
|
||||
|
||||
#[test]
|
||||
fn out_of_class_method_def() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
void ns::Foo::method() { }
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(
|
||||
@@ -761,7 +761,7 @@ void ns::Foo::method() { }
|
||||
|
||||
#[test]
|
||||
fn template_declaration() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
template<typename T>
|
||||
class Bar {
|
||||
void tmpl_method() {}
|
||||
@@ -769,7 +769,7 @@ class Bar {
|
||||
|
||||
template<typename T>
|
||||
void tmpl_free_fn(T x) {}
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(s.iter().any(|x| x == "Bar"), "Bar class missing: {s:?}");
|
||||
@@ -785,12 +785,12 @@ void tmpl_free_fn(T x) {}
|
||||
|
||||
#[test]
|
||||
fn enum_and_concept() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
enum class Color { Red, Green };
|
||||
|
||||
template<typename T>
|
||||
concept Printable = requires(T t) { t.print(); };
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(s.iter().any(|x| x == "Color"), "Color missing: {s:?}");
|
||||
@@ -813,11 +813,11 @@ extern "C" {
|
||||
|
||||
#[test]
|
||||
fn conversion_operator() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
class Foo {
|
||||
operator bool() const { return true; }
|
||||
};
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(
|
||||
@@ -852,11 +852,11 @@ class Foo {
|
||||
|
||||
#[test]
|
||||
fn ref_returning_operator() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
class Foo {
|
||||
Foo& operator=(const Foo& o) { return *this; }
|
||||
};
|
||||
"#;
|
||||
";
|
||||
let doc = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
let s = syms(&doc);
|
||||
assert!(
|
||||
@@ -867,14 +867,14 @@ class Foo {
|
||||
|
||||
#[test]
|
||||
fn deterministic_across_runs() {
|
||||
let src = r#"
|
||||
let src = r"
|
||||
namespace ns {
|
||||
class Foo {
|
||||
void method() {}
|
||||
};
|
||||
}
|
||||
void free_fn() {}
|
||||
"#;
|
||||
";
|
||||
let a = tests_support::extract_cpp(src, "x/foo.cpp");
|
||||
for _ in 0..20 {
|
||||
assert_eq!(tests_support::extract_cpp(src, "x/foo.cpp").blocks, a.blocks);
|
||||
|
||||
@@ -315,7 +315,7 @@ fn build_blocks(
|
||||
// mod-prefix-agnostic.
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -335,7 +335,7 @@ fn build_blocks(
|
||||
lang: Some("go".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -248,7 +248,7 @@ fn build_blocks(
|
||||
// post-pass as 1B / 1C-Go).
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -268,7 +268,7 @@ fn build_blocks(
|
||||
lang: Some("java".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -293,7 +293,7 @@ fn build_blocks(
|
||||
let inner_kind = inner.kind();
|
||||
match inner_kind {
|
||||
"function_declaration" | "class_declaration" => {
|
||||
let name_opt = name_text(&inner, src).map(|s| s.to_string());
|
||||
let name_opt = name_text(&inner, src).map(std::string::ToString::to_string);
|
||||
if let Some(name) = name_opt {
|
||||
glue.retain(|(_, gs, _)| *gs < outer_s);
|
||||
flush_glue(glue, units, mod_prefix, mod_path);
|
||||
@@ -332,7 +332,7 @@ fn build_blocks(
|
||||
| "function_declaration"
|
||||
| "class"
|
||||
| "class_declaration" => {
|
||||
let name_opt = name_text(&value, src).map(|s| s.to_string());
|
||||
let name_opt = name_text(&value, src).map(std::string::ToString::to_string);
|
||||
let leaf =
|
||||
name_opt.as_deref().unwrap_or("default").to_string();
|
||||
glue.retain(|(_, gs, _)| *gs < outer_s);
|
||||
@@ -402,7 +402,7 @@ fn build_blocks(
|
||||
// post-pass as 1A Gap 1 / Python / TS).
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -422,7 +422,7 @@ fn build_blocks(
|
||||
lang: Some("javascript".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -290,7 +290,7 @@ fn build_blocks(
|
||||
// post-pass as 1B / 1C-Go / Java).
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -310,7 +310,7 @@ fn build_blocks(
|
||||
lang: Some("kotlin".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -333,7 +333,7 @@ fn build_blocks(
|
||||
// future-proofed) still demotes correctly.
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -353,7 +353,7 @@ fn build_blocks(
|
||||
lang: Some("python".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -336,7 +336,7 @@ fn build_blocks(
|
||||
// group is `<top-level>`, even a pure mod-decl group.
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
// Match on the *suffix*: a glue group may now carry a module
|
||||
// prefix (`inner::<module>`), so demote any `…<module>` to the
|
||||
// same-prefixed `…<top-level>` rather than only the bare form.
|
||||
@@ -359,7 +359,7 @@ fn build_blocks(
|
||||
lang: Some("rust".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -326,7 +326,7 @@ fn build_blocks(
|
||||
| "interface_declaration"
|
||||
| "type_alias_declaration"
|
||||
| "enum_declaration" => {
|
||||
let name_opt = name_text(&inner, src).map(|s| s.to_string());
|
||||
let name_opt = name_text(&inner, src).map(std::string::ToString::to_string);
|
||||
if let Some(name) = name_opt {
|
||||
glue.retain(|(_, gs, _)| *gs < outer_s);
|
||||
flush_glue(glue, units, mod_prefix, mod_path);
|
||||
@@ -376,7 +376,7 @@ fn build_blocks(
|
||||
| "class"
|
||||
| "class_declaration" => {
|
||||
let name_opt =
|
||||
name_text(&value, src).map(|s| s.to_string());
|
||||
name_text(&value, src).map(std::string::ToString::to_string);
|
||||
let leaf = name_opt
|
||||
.as_deref()
|
||||
.unwrap_or("default")
|
||||
@@ -461,7 +461,7 @@ fn build_blocks(
|
||||
// post-pass as 1A Gap 1 / Python).
|
||||
let has_real_unit = units.iter().any(|(_, _, _, is_real)| *is_real);
|
||||
if has_real_unit {
|
||||
for (sym, _, _, is_real) in units.iter_mut() {
|
||||
for (sym, _, _, is_real) in &mut units {
|
||||
if !*is_real && sym.ends_with("<module>") {
|
||||
let pre = &sym[..sym.len() - "<module>".len()];
|
||||
*sym = format!("{pre}<top-level>");
|
||||
@@ -481,7 +481,7 @@ fn build_blocks(
|
||||
lang: Some("typescript".to_string()),
|
||||
};
|
||||
let block_id = id_for_block(doc_id, "code", &[], ordinal as u32, &span);
|
||||
let code = lines[(line_start as usize - 1)..=(line_end as usize - 1)].join("\n");
|
||||
let code = lines[(line_start as usize - 1)..(line_end as usize)].join("\n");
|
||||
blocks.push(Block::Code(CodeBlock {
|
||||
common: CommonBlock {
|
||||
block_id,
|
||||
|
||||
@@ -55,3 +55,6 @@ base64 = { workspace = true }
|
||||
# at runtime) is preserved.
|
||||
kebab-llm = { path = "../kebab-llm", features = ["mock"] }
|
||||
kebab-llm-local = { path = "../kebab-llm-local" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -198,7 +198,7 @@ pub fn apply_caption(
|
||||
/// language; everything else falls through to English.
|
||||
fn build_prompt(lang_hint: Option<&str>) -> (String, String) {
|
||||
match lang_hint {
|
||||
Some("ko") | Some("kor") => (
|
||||
Some("ko" | "kor") => (
|
||||
"이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \
|
||||
보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \
|
||||
한 문장만 출력."
|
||||
|
||||
@@ -103,7 +103,7 @@ fn ascii_field(exif: &exif::Exif, tag: Tag) -> Option<String> {
|
||||
fn u32_field(exif: &exif::Exif, tag: Tag) -> Option<u32> {
|
||||
let f = exif.get_field(tag, In::PRIMARY)?;
|
||||
match &f.value {
|
||||
Value::Short(v) => v.first().map(|x| *x as u32),
|
||||
Value::Short(v) => v.first().map(|x| u32::from(*x)),
|
||||
Value::Long(v) => v.first().copied(),
|
||||
_ => None,
|
||||
}
|
||||
@@ -177,7 +177,7 @@ fn rational_to_f64(r: &exif::Rational) -> Option<f64> {
|
||||
if r.denom == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(r.num as f64 / r.denom as f64)
|
||||
Some(f64::from(r.num) / f64::from(r.denom))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -162,9 +162,7 @@ mod tests {
|
||||
let ratio = w as f32 / h as f32;
|
||||
assert!(
|
||||
(ratio - 4.0 / 3.0).abs() < 0.02,
|
||||
"aspect drift: in=4/3 out={}/{}={ratio}",
|
||||
w,
|
||||
h
|
||||
"aspect drift: in=4/3 out={w}/{h}={ratio}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
let app1_payload_len = 2 + 6 + exif_blob.len();
|
||||
assert!(
|
||||
app1_payload_len <= u16::MAX as usize,
|
||||
u16::try_from(app1_payload_len).is_ok(),
|
||||
"EXIF segment too large for a single APP1"
|
||||
);
|
||||
out.extend_from_slice(&(app1_payload_len as u16).to_be_bytes());
|
||||
|
||||
@@ -80,8 +80,8 @@ fn jpeg_with_exif_gps_captures_whitelisted_tags() {
|
||||
Some(&Value::String("2024-08-15T12:34:56".into()))
|
||||
);
|
||||
assert_eq!(exif.get("orientation"), Some(&Value::Number(1.into())));
|
||||
let lat = exif.get("gps_lat").and_then(|v| v.as_f64()).expect("gps_lat");
|
||||
let lon = exif.get("gps_lon").and_then(|v| v.as_f64()).expect("gps_lon");
|
||||
let lat = exif.get("gps_lat").and_then(serde_json::Value::as_f64).expect("gps_lat");
|
||||
let lon = exif.get("gps_lon").and_then(serde_json::Value::as_f64).expect("gps_lon");
|
||||
assert!((lat - 37.5).abs() < 1e-6, "lat={lat}");
|
||||
assert!((lon - 127.0).abs() < 1e-6, "lon={lon}");
|
||||
|
||||
@@ -281,7 +281,7 @@ fn jpeg_with_gps_out_of_range_drops_latitude() {
|
||||
!exif.contains_key("gps_lat"),
|
||||
"out-of-range latitude must be dropped"
|
||||
);
|
||||
let lon = exif.get("gps_lon").and_then(|v| v.as_f64()).expect("gps_lon");
|
||||
let lon = exif.get("gps_lon").and_then(serde_json::Value::as_f64).expect("gps_lon");
|
||||
assert!((lon - 127.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ async fn ocr_integration_real_ollama_transcribes_text() {
|
||||
.expect("blocking task panicked")
|
||||
.expect("real Ollama OCR must succeed");
|
||||
eprintln!("integration OCR result: {:?}", text.joined);
|
||||
let normalized = text.joined.to_lowercase().replace(",", "").replace(".", "");
|
||||
let normalized = text.joined.to_lowercase().replace(',', "").replace('.', "");
|
||||
assert!(
|
||||
normalized.contains("hello") && normalized.contains("world"),
|
||||
"integration OCR did not capture expected text: {:?}",
|
||||
|
||||
@@ -38,3 +38,6 @@ lingua = { version = "1.8", default-features = false, features = [
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -60,18 +60,15 @@ pub fn parse_blocks(
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
parse_blocks_inner(body, body_offset_lines)
|
||||
}));
|
||||
match result {
|
||||
Ok(out) => Ok(out),
|
||||
Err(_) => {
|
||||
tracing::warn!("parse_blocks panicked on adversarial input; returning empty");
|
||||
Ok((
|
||||
Vec::new(),
|
||||
vec![Warning {
|
||||
kind: WarningKind::ExtractFailed,
|
||||
note: "pulldown-cmark panicked; body discarded".to_string(),
|
||||
}],
|
||||
))
|
||||
}
|
||||
if let Ok(out) = result { Ok(out) } else {
|
||||
tracing::warn!("parse_blocks panicked on adversarial input; returning empty");
|
||||
Ok((
|
||||
Vec::new(),
|
||||
vec![Warning {
|
||||
kind: WarningKind::ExtractFailed,
|
||||
note: "pulldown-cmark panicked; body discarded".to_string(),
|
||||
}],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +99,7 @@ fn parse_blocks_inner(body: &[u8], body_offset_lines: u32) -> (Vec<ParsedBlock>,
|
||||
// possibly-inverted spans would be more harmful than dropping output.
|
||||
if state.overflow_detected {
|
||||
let at = state
|
||||
.overflow_at_body_line
|
||||
.map(|n| n.to_string())
|
||||
.unwrap_or_else(|| "?".to_string());
|
||||
.overflow_at_body_line.map_or_else(|| "?".to_string(), |n| n.to_string());
|
||||
return (
|
||||
Vec::new(),
|
||||
vec![Warning {
|
||||
@@ -339,10 +334,10 @@ impl InlineBuf {
|
||||
// `Inline::Link.text` field. Code/strong/emph inside a link are
|
||||
// collapsed to their plain text — `Inline::Link` doesn't model
|
||||
// formatting inside the link.
|
||||
let flat = if !text.is_empty() {
|
||||
text
|
||||
} else {
|
||||
let flat = if text.is_empty() {
|
||||
flatten_inlines_to_text(&kids)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
self.push_inline(Inline::Link { text: flat, href });
|
||||
}
|
||||
@@ -364,10 +359,10 @@ impl InlineBuf {
|
||||
InlineFrame::Strong(kids) => self.push_inline(Inline::Strong { children: kids }),
|
||||
InlineFrame::Emph(kids) => self.push_inline(Inline::Emph { children: kids }),
|
||||
InlineFrame::Link { href, text, kids } => {
|
||||
let flat = if !text.is_empty() {
|
||||
text
|
||||
} else {
|
||||
let flat = if text.is_empty() {
|
||||
flatten_inlines_to_text(&kids)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
self.push_inline(Inline::Link { text: flat, href });
|
||||
}
|
||||
@@ -528,20 +523,17 @@ impl<'a> WalkState<'a> {
|
||||
// inverted span. Without this guard, debug builds panic with
|
||||
// "attempt to add with overflow" (caught by `catch_unwind`, masking
|
||||
// the real cause) and release builds wrap to `start > end`.
|
||||
match (
|
||||
if let (Some(start), Some(end)) = (
|
||||
start_body.checked_add(self.body_offset_lines),
|
||||
end_body.checked_add(self.body_offset_lines),
|
||||
) {
|
||||
(Some(start), Some(end)) => SourceSpan::Line { start, end },
|
||||
_ => {
|
||||
if !self.overflow_detected {
|
||||
self.overflow_detected = true;
|
||||
self.overflow_at_body_line = Some(start_body);
|
||||
}
|
||||
SourceSpan::Line {
|
||||
start: start_body.saturating_add(self.body_offset_lines),
|
||||
end: end_body.saturating_add(self.body_offset_lines),
|
||||
}
|
||||
) { SourceSpan::Line { start, end } } else {
|
||||
if !self.overflow_detected {
|
||||
self.overflow_detected = true;
|
||||
self.overflow_at_body_line = Some(start_body);
|
||||
}
|
||||
SourceSpan::Line {
|
||||
start: start_body.saturating_add(self.body_offset_lines),
|
||||
end: end_body.saturating_add(self.body_offset_lines),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,11 +669,11 @@ impl<'a> WalkState<'a> {
|
||||
}
|
||||
Event::Start(Tag::Strong) => {
|
||||
self.flag_non_image_in_paragraph();
|
||||
self.with_current_inlines(|buf| buf.open_strong());
|
||||
self.with_current_inlines(InlineBuf::open_strong);
|
||||
}
|
||||
Event::Start(Tag::Emphasis) => {
|
||||
self.flag_non_image_in_paragraph();
|
||||
self.with_current_inlines(|buf| buf.open_emph());
|
||||
self.with_current_inlines(InlineBuf::open_emph);
|
||||
}
|
||||
Event::Start(Tag::Link { dest_url, .. }) => {
|
||||
self.flag_non_image_in_paragraph();
|
||||
@@ -991,13 +983,13 @@ impl<'a> WalkState<'a> {
|
||||
}
|
||||
}
|
||||
Event::End(TagEnd::Strong) => {
|
||||
self.with_current_inlines(|buf| buf.close_strong());
|
||||
self.with_current_inlines(InlineBuf::close_strong);
|
||||
}
|
||||
Event::End(TagEnd::Emphasis) => {
|
||||
self.with_current_inlines(|buf| buf.close_emph());
|
||||
self.with_current_inlines(InlineBuf::close_emph);
|
||||
}
|
||||
Event::End(TagEnd::Link) => {
|
||||
self.with_current_inlines(|buf| buf.close_link());
|
||||
self.with_current_inlines(InlineBuf::close_link);
|
||||
}
|
||||
Event::End(TagEnd::Image) => {
|
||||
if let Some(Frame::Paragraph { image_depth, .. }) = self.frames.last_mut() {
|
||||
@@ -1480,8 +1472,7 @@ mod tests {
|
||||
inl,
|
||||
Inline::Text { .. } | Inline::Code { .. } | Inline::Link { .. } | Inline::Strong { .. } | Inline::Emph { .. }
|
||||
),
|
||||
"unexpected inline kind: {:?}",
|
||||
inl
|
||||
"unexpected inline kind: {inl:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1503,7 +1494,7 @@ mod tests {
|
||||
// First item should contain "a" plus a flattened rendering
|
||||
// of the nested sub-list.
|
||||
let flat = flatten_inlines_to_text(&items[0]);
|
||||
assert!(flat.contains("a"), "first item missing 'a': {flat:?}");
|
||||
assert!(flat.contains('a'), "first item missing 'a': {flat:?}");
|
||||
assert!(flat.contains("- x"), "first item missing '- x': {flat:?}");
|
||||
assert!(flat.contains("- y"), "first item missing '- y': {flat:?}");
|
||||
let flat2 = flatten_inlines_to_text(&items[1]);
|
||||
|
||||
@@ -110,7 +110,7 @@ pub fn parse_frontmatter(
|
||||
}
|
||||
};
|
||||
|
||||
let body_start = span_opt.map(|s| s.end).unwrap_or(0);
|
||||
let body_start = span_opt.map_or(0, |s| s.end);
|
||||
let body = &bytes[body_start..];
|
||||
|
||||
let metadata = derive_metadata(raw_opt, hints, body, &mut warnings);
|
||||
@@ -430,30 +430,24 @@ fn derive_metadata(
|
||||
// ---- source_type ----
|
||||
let source_type = match raw.source_type.as_deref() {
|
||||
None => SourceType::Markdown,
|
||||
Some(s) => match parse_source_type(s) {
|
||||
Some(st) => st,
|
||||
None => {
|
||||
warnings.push(Warning {
|
||||
kind: WarningKind::MalformedFrontmatter,
|
||||
note: format!("unknown source_type={s}, defaulted to markdown"),
|
||||
});
|
||||
SourceType::Markdown
|
||||
}
|
||||
Some(s) => if let Some(st) = parse_source_type(s) { st } else {
|
||||
warnings.push(Warning {
|
||||
kind: WarningKind::MalformedFrontmatter,
|
||||
note: format!("unknown source_type={s}, defaulted to markdown"),
|
||||
});
|
||||
SourceType::Markdown
|
||||
},
|
||||
};
|
||||
|
||||
// ---- trust_level ----
|
||||
let trust_level = match raw.trust_level.as_deref() {
|
||||
None => TrustLevel::Primary,
|
||||
Some(s) => match parse_trust_level(s) {
|
||||
Some(tl) => tl,
|
||||
None => {
|
||||
warnings.push(Warning {
|
||||
kind: WarningKind::MalformedFrontmatter,
|
||||
note: format!("unknown trust_level={s}, defaulted to primary"),
|
||||
});
|
||||
TrustLevel::Primary
|
||||
}
|
||||
Some(s) => if let Some(tl) = parse_trust_level(s) { tl } else {
|
||||
warnings.push(Warning {
|
||||
kind: WarningKind::MalformedFrontmatter,
|
||||
note: format!("unknown trust_level={s}, defaulted to primary"),
|
||||
});
|
||||
TrustLevel::Primary
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,3 +24,6 @@ lopdf = "0.32"
|
||||
|
||||
[dev-dependencies]
|
||||
blake3 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -111,7 +111,7 @@ impl Extractor for PdfTextExtractor {
|
||||
});
|
||||
|
||||
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) {
|
||||
Ok(t) if !t.trim().is_empty() => (t, None),
|
||||
Ok(_) => (
|
||||
|
||||
@@ -10,3 +10,6 @@ description = "Parser intermediate representations (no parser libs allowed)"
|
||||
[dependencies]
|
||||
kebab-core = { path = "../kebab-core" }
|
||||
serde = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -28,3 +28,6 @@ kebab-llm = { path = "../kebab-llm", features = ["mock"] }
|
||||
tempfile = { workspace = true }
|
||||
rusqlite = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -318,7 +318,7 @@ impl RagPipeline {
|
||||
});
|
||||
}
|
||||
let chunks_returned = u32::try_from(hits.len()).unwrap_or(u32::MAX);
|
||||
let top_score = hits.first().map(|h| h.retrieval.fusion_score).unwrap_or(0.0);
|
||||
let top_score = hits.first().map_or(0.0, |h| h.retrieval.fusion_score);
|
||||
|
||||
tracing::debug!(
|
||||
target: "kebab-rag",
|
||||
@@ -856,7 +856,7 @@ impl RagPipeline {
|
||||
});
|
||||
}
|
||||
let chunks_returned = u32::try_from(pool.len()).unwrap_or(u32::MAX);
|
||||
let top_score = pool.first().map(|h| h.retrieval.fusion_score).unwrap_or(0.0);
|
||||
let top_score = pool.first().map_or(0.0, |h| h.retrieval.fusion_score);
|
||||
|
||||
// ── 3. Score gate / no chunks ──────────────────────────────────────
|
||||
// PR-3b-ii: forward the partial hop trace into the refusal so
|
||||
@@ -1149,7 +1149,7 @@ impl RagPipeline {
|
||||
refusal_phrase_detected = matched_refusal_phrase,
|
||||
finish_reason = ?finish_reason,
|
||||
chunks_used,
|
||||
hops = answer.hops.as_ref().map(|v| v.len()).unwrap_or(0),
|
||||
hops = answer.hops.as_ref().map_or(0, std::vec::Vec::len),
|
||||
"kb-rag: multi-hop ask done"
|
||||
);
|
||||
|
||||
@@ -1388,16 +1388,13 @@ impl RagPipeline {
|
||||
let chunk_full =
|
||||
<SqliteStore as kebab_core::DocumentStore>::get_chunk(&self.docs, &hit.chunk_id)
|
||||
.context("kb-rag: docs.get_chunk")?;
|
||||
let chunk_text = match chunk_full {
|
||||
Some(c) => c.text,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
target: "kebab-rag",
|
||||
chunk_id = %hit.chunk_id.0,
|
||||
"kb-rag: chunk not found in store; skipping"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let chunk_text = if let Some(c) = chunk_full { c.text } else {
|
||||
tracing::warn!(
|
||||
target: "kebab-rag",
|
||||
chunk_id = %hit.chunk_id.0,
|
||||
"kb-rag: chunk not found in store; skipping"
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let header = format!(
|
||||
"[#{n}] doc={} heading={} span={}\n",
|
||||
@@ -1999,13 +1996,11 @@ fn strip_markdown_json_fence(s: &str) -> &str {
|
||||
let after_open = trimmed
|
||||
.strip_prefix("```json")
|
||||
.or_else(|| trimmed.strip_prefix("```"))
|
||||
.map(|rest| rest.trim_start_matches('\n'))
|
||||
.unwrap_or(trimmed);
|
||||
.map_or(trimmed, |rest| rest.trim_start_matches('\n'));
|
||||
let inner = after_open
|
||||
.trim_end()
|
||||
.strip_suffix("```")
|
||||
.map(|rest| rest.trim_end())
|
||||
.unwrap_or(after_open);
|
||||
.map_or(after_open, str::trim_end);
|
||||
inner.trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ pub fn mk_hit_with_indexed_at(
|
||||
chunk_id: ChunkId(chunk_id.to_string()),
|
||||
doc_id: DocumentId(doc_id.to_string()),
|
||||
doc_path: p.clone(),
|
||||
heading_path: heading.iter().map(|s| s.to_string()).collect(),
|
||||
heading_path: heading.iter().map(std::string::ToString::to_string).collect(),
|
||||
section_label: None,
|
||||
snippet: "snippet".to_string(),
|
||||
citation: Citation::Line {
|
||||
|
||||
@@ -68,7 +68,7 @@ fn multi_hop_decide_stop_triggers_synthesize() {
|
||||
// Three LLM calls in order: decompose → decide → synthesize.
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
"answer body [#1]",
|
||||
]));
|
||||
let lm_handle = lm.clone();
|
||||
@@ -131,7 +131,7 @@ fn multi_hop_decide_continue_adds_more_chunks() {
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1"]"#,
|
||||
r#"["q2"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
"synthesized [#1] [#2]",
|
||||
]));
|
||||
let lm_handle = lm.clone();
|
||||
@@ -255,7 +255,7 @@ fn multi_hop_pool_chunks_dedup_by_chunk_id() {
|
||||
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1", "q2"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
"merged answer [#1]",
|
||||
]));
|
||||
let lm_handle = lm.clone();
|
||||
@@ -444,7 +444,7 @@ fn multi_hop_refuse_score_gate_preserves_hops_trace() {
|
||||
// never runs because we refuse before pack_context.
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
]));
|
||||
let lm_handle = lm.clone();
|
||||
let lm_dyn: Arc<dyn LanguageModel> = lm;
|
||||
@@ -594,7 +594,7 @@ fn multi_hop_above_probe_gate_proceeds_to_decompose() {
|
||||
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
"answer [#1]",
|
||||
]));
|
||||
let lm_handle = lm.clone();
|
||||
@@ -649,7 +649,7 @@ fn happy_multi_hop_env() -> (RagEnv, Arc<ScriptedRetriever>, Arc<ScriptedLm>) {
|
||||
let retriever = Arc::new(ScriptedRetriever::new(vec![hits.clone(), hits]));
|
||||
let lm = Arc::new(ScriptedLm::new(vec![
|
||||
r#"["q1"]"#,
|
||||
r#"[]"#,
|
||||
r"[]",
|
||||
"answer body [#1]",
|
||||
]));
|
||||
(env, retriever, lm)
|
||||
|
||||
@@ -282,8 +282,7 @@ fn streaming_forwards_tokens_to_sink() {
|
||||
StreamEvent::Token { delta, .. } => Some(delta),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
.collect::<String>();
|
||||
assert_eq!(collected, canned);
|
||||
}
|
||||
|
||||
@@ -522,7 +521,7 @@ fn answer_json_serializes_with_expected_keys() {
|
||||
let answer = pipeline.ask("what", default_opts()).unwrap();
|
||||
let v: serde_json::Value = serde_json::to_value(&answer).unwrap();
|
||||
// Stable top-level key set per `answer.v1` (§2.3).
|
||||
let keys: Vec<&str> = v.as_object().unwrap().keys().map(|s| s.as_str()).collect();
|
||||
let keys: Vec<&str> = v.as_object().unwrap().keys().map(std::string::String::as_str).collect();
|
||||
for needed in [
|
||||
"answer",
|
||||
"citations",
|
||||
|
||||
@@ -36,3 +36,6 @@ tempfile = { workspace = true }
|
||||
# The mock-retriever unit tests (the bulk of the hybrid suite) do not
|
||||
# need either, but the integration / snapshot lane does.
|
||||
kebab-embed = { path = "../kebab-embed", features = ["mock"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -601,7 +601,7 @@ mod tests {
|
||||
let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 5);
|
||||
let out = h.search(&make_query(SearchMode::Hybrid, 5)).unwrap();
|
||||
let a = out.iter().find(|h| h.chunk_id.0 == "aaaa").unwrap();
|
||||
let actual = a.retrieval.fusion_score as f64;
|
||||
let actual = f64::from(a.retrieval.fusion_score);
|
||||
// Tolerance: the score is computed in f64 and cast to f32 at
|
||||
// the API boundary, so any discrepancy must fit within f32
|
||||
// precision. `1e-7` is below `f32::EPSILON` (~1.19e-7), which
|
||||
@@ -694,7 +694,7 @@ mod tests {
|
||||
let h = HybridRetriever::with_policy(lex, vec, rrf_policy(60), 4);
|
||||
let out = h.search(&make_query(SearchMode::Hybrid, 4)).unwrap();
|
||||
let mut ids: Vec<&str> = out.iter().map(|h| h.chunk_id.0.as_str()).collect();
|
||||
ids.sort();
|
||||
ids.sort_unstable();
|
||||
assert_eq!(ids, vec!["aaaa", "bbbb", "cccc", "dddd"]);
|
||||
}
|
||||
|
||||
|
||||
@@ -457,7 +457,7 @@ fn run_query(
|
||||
.prepare(&sql)
|
||||
.context("kb-search lexical: prepare FTS5 statement")?;
|
||||
let rows = stmt
|
||||
.query_map(params_from_iter(params.iter().map(|b| b.as_ref())), row_from_sql)
|
||||
.query_map(params_from_iter(params.iter().map(std::convert::AsRef::as_ref)), row_from_sql)
|
||||
.context("kb-search lexical: execute FTS5 query")?;
|
||||
let mut out: Vec<RawRow> = Vec::new();
|
||||
for r in rows {
|
||||
|
||||
@@ -37,12 +37,10 @@ use tempfile::TempDir;
|
||||
pub fn require_avx_or_panic() {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
if !std::is_x86_feature_detected!("avx") {
|
||||
panic!(
|
||||
"kb-search hybrid integration test requires AVX-capable hardware; \
|
||||
host CPU lacks AVX. Run on an AVX-capable machine."
|
||||
);
|
||||
}
|
||||
assert!(std::is_x86_feature_detected!("avx"),
|
||||
"kb-search hybrid integration test requires AVX-capable hardware; \
|
||||
host CPU lacks AVX. Run on an AVX-capable machine."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +283,7 @@ impl HybridEnv {
|
||||
vector,
|
||||
doc_id: DocumentId(doc_id.to_string()),
|
||||
text: text.to_string(),
|
||||
heading_path: heading_path.iter().map(|s| s.to_string()).collect(),
|
||||
heading_path: heading_path.iter().map(std::string::ToString::to_string).collect(),
|
||||
model_id: EmbeddingModelId(TEST_MODEL_ID.to_string()),
|
||||
model_version: EmbeddingVersion("v1".to_string()),
|
||||
dimensions: TEST_DIMENSIONS,
|
||||
|
||||
@@ -186,14 +186,12 @@ fn hybrid_snapshot_run_1() {
|
||||
// Refuse to silently "pass" against the committed placeholder. The
|
||||
// placeholder JSON carries a `_comment` field with regeneration
|
||||
// instructions; production fixtures (a captured list) do not.
|
||||
if expected.get("_comment").is_some() {
|
||||
panic!(
|
||||
"snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \
|
||||
Path: {}. To regenerate: \
|
||||
`KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.",
|
||||
fixture.display()
|
||||
);
|
||||
}
|
||||
assert!(!expected.get("_comment").is_some(),
|
||||
"snapshot fixture is a placeholder — regenerate on AVX hardware then commit. \
|
||||
Path: {}. To regenerate: \
|
||||
`KEBAB_UPDATE_SNAPSHOTS=1 cargo test -p kb-search -- --ignored hybrid_snapshot`.",
|
||||
fixture.display()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
|
||||
@@ -23,3 +23,6 @@ globset = "0.4"
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
tempfile = "3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(crate) fn media_type_for(path: &Path) -> MediaType {
|
||||
let ext = path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.to_ascii_lowercase())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.unwrap_or_default();
|
||||
|
||||
match ext.as_str() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user