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"
|
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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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((
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}"))?;
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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\""));
|
||||||
|
|||||||
@@ -14,13 +14,11 @@ 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."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// First run downloads ~470MB; expect ~30-60s warm, several minutes cold.
|
// First run downloads ~470MB; expect ~30-60s warm, several minutes cold.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,8 +1171,7 @@ 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);
|
||||||
@@ -1180,7 +1179,6 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
println!("no orphaned docs found — store is already in sync with walker scope");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -22,3 +22,6 @@ tracing = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -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") };
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -226,8 +226,7 @@ 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:?})"))?;
|
||||||
@@ -241,13 +240,11 @@ fn parse_hms_ms(s: &str) -> Result<u64> {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?;
|
.map_err(|_| anyhow::anyhow!("bad milliseconds in {ms_part:?} (input {s:?})"))?;
|
||||||
(sec, ms)
|
(sec, ms)
|
||||||
}
|
} else {
|
||||||
None => {
|
|
||||||
let sec: u64 = parts[2]
|
let sec: u64 = parts[2]
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?;
|
.map_err(|_| anyhow::anyhow!("bad seconds in {:?} (input {s:?})", parts[2]))?;
|
||||||
(sec, 0)
|
(sec, 0)
|
||||||
}
|
|
||||||
};
|
};
|
||||||
Ok(h * 3_600_000 + m * 60_000 + sec * 1000 + ms)
|
Ok(h * 3_600_000 + m * 60_000 + sec * 1000 + ms)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")?;
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ mock = []
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = { workspace = true }
|
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).
|
/// 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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -19,3 +19,6 @@ mock = []
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
proptest = { workspace = true }
|
proptest = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ kebab-core = { path = "../kebab-core" }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
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.
|
// 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"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:?}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -28,3 +28,6 @@ tracing = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -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]]];
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ tree-sitter-cpp = { workspace = true }
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
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>"
|
// 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!(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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") => (
|
||||||
"이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \
|
"이미지를 한 문장으로 객관적으로 설명한다. 추측은 피하고, \
|
||||||
보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \
|
보이는 것만 적는다. 마크다운 / 따옴표 / 부가 설명 없이 \
|
||||||
한 문장만 출력."
|
한 문장만 출력."
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: {:?}",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ 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),
|
|
||||||
Err(_) => {
|
|
||||||
tracing::warn!("parse_blocks panicked on adversarial input; returning empty");
|
tracing::warn!("parse_blocks panicked on adversarial input; returning empty");
|
||||||
Ok((
|
Ok((
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -72,7 +70,6 @@ pub fn parse_blocks(
|
|||||||
}],
|
}],
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_blocks_inner(body: &[u8], body_offset_lines: u32) -> (Vec<ParsedBlock>, Vec<Warning>) {
|
fn parse_blocks_inner(body: &[u8], body_offset_lines: u32) -> (Vec<ParsedBlock>, Vec<Warning>) {
|
||||||
@@ -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,12 +523,10 @@ 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 {
|
if !self.overflow_detected {
|
||||||
self.overflow_detected = true;
|
self.overflow_detected = true;
|
||||||
self.overflow_at_body_line = Some(start_body);
|
self.overflow_at_body_line = Some(start_body);
|
||||||
@@ -544,7 +537,6 @@ impl<'a> WalkState<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Where to emit a finished block: into the current container if any
|
/// Where to emit a finished block: into the current container if any
|
||||||
/// (Quote / ListItem), otherwise into the top-level `blocks` vec.
|
/// (Quote / ListItem), otherwise into the top-level `blocks` vec.
|
||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
None => {
|
|
||||||
warnings.push(Warning {
|
warnings.push(Warning {
|
||||||
kind: WarningKind::MalformedFrontmatter,
|
kind: WarningKind::MalformedFrontmatter,
|
||||||
note: format!("unknown source_type={s}, defaulted to markdown"),
|
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,
|
|
||||||
None => {
|
|
||||||
warnings.push(Warning {
|
warnings.push(Warning {
|
||||||
kind: WarningKind::MalformedFrontmatter,
|
kind: WarningKind::MalformedFrontmatter,
|
||||||
note: format!("unknown trust_level={s}, defaulted to primary"),
|
note: format!("unknown trust_level={s}, defaulted to primary"),
|
||||||
});
|
});
|
||||||
TrustLevel::Primary
|
TrustLevel::Primary
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,3 +24,6 @@ lopdf = "0.32"
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
blake3 = { workspace = true }
|
blake3 = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -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(_) => (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
|
||||||
None => {
|
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
target: "kebab-rag",
|
target: "kebab-rag",
|
||||||
chunk_id = %hit.chunk_id.0,
|
chunk_id = %hit.chunk_id.0,
|
||||||
"kb-rag: chunk not found in store; skipping"
|
"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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -37,13 +37,11 @@ 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."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Index version label used by hybrid integration tests so the
|
/// Index version label used by hybrid integration tests so the
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user