refactor(app): ingest API 6변종 → 2 (ingest + ingest_with_config{IngestOpts})

summary_only를 IngestOpts에 흡수, progress/cancellable/opts 변종 제거.
재인덱싱 게이트 CHUNKS/SEARCH/ASK byte-IDENTICAL, clippy --all-targets 0.
This commit is contained in:
2026-06-24 12:36:20 +00:00
parent d24f9641d5
commit d501ce2626
36 changed files with 223 additions and 250 deletions

View File

@@ -1,4 +1,4 @@
//! Streaming progress events for `ingest_with_config_progress`.
//! Streaming progress events for `ingest_with_config` (via `IngestOpts::progress`).
//!
//! The facade emits one [`IngestEvent`] per step boundary into an
//! optional `mpsc::Sender<IngestEvent>` injected by the caller. CLI

View File

@@ -183,70 +183,47 @@ fn load_config() -> anyhow::Result<kebab_config::Config> {
// ── ingest ────────────────────────────────────────────────────────────────
/// p9-fb-23: optional per-call ingest controls. Kept as a struct (vs.
/// a growing positional arg list) so future flags (e.g. `dry_run`,
/// per-asset `concurrency`) land additively without churning every
/// caller. Mirrors the `AskOpts` pattern from p9-fb-15.
/// Per-call ingest controls. Kept as a struct (vs. a growing positional
/// arg list) so future flags (e.g. `dry_run`, per-asset `concurrency`)
/// land additively without churning every caller. Mirrors the `AskOpts`
/// pattern from p9-fb-15.
///
/// `summary_only` was formerly a positional arg on every ingest entry
/// point; it lives here now (Phase 3 Unit 3.1 collapse).
#[derive(Default)]
pub struct IngestOpts {
/// Streaming progress sink. `None` suppresses emission entirely.
pub progress: Option<std::sync::mpsc::Sender<crate::ingest_progress::IngestEvent>>,
/// Cooperative cancel token. `None` = uncancellable.
pub cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
/// p9-fb-23: when `true`, the per-asset early-skip block is bypassed
/// — every asset is re-parsed / re-chunked / re-embedded as if the
/// DB were empty. Default `false` preserves the auto-skip path.
/// When `true`, the per-asset early-skip block is bypassed — every
/// asset is re-parsed / re-chunked / re-embedded as if the DB were
/// empty. Default `false` preserves the auto-skip path.
pub force_reingest: bool,
/// When `true`, only chunk/index metadata is written; embeddings are
/// skipped. Equivalent to the former positional `summary_only` arg.
pub summary_only: bool,
}
pub fn ingest(scope: SourceScope, summary_only: bool) -> anyhow::Result<IngestReport> {
/// Facade entry point — loads [`kebab_config::Config`] from the XDG
/// default path, then forwards to [`ingest_with_config`].
///
/// Per the facade rule: the bare `ingest` form always re-loads the XDG
/// config. Callers with an explicit config (CLI `--config`, tests, TUI)
/// should call [`ingest_with_config`] directly.
pub fn ingest(scope: SourceScope, opts: IngestOpts) -> anyhow::Result<IngestReport> {
let config = load_config()?;
ingest_with_config(config, scope, summary_only)
ingest_with_config(config, scope, opts)
}
/// Config-explicit variant — bypasses [`load_config`] when the
/// caller (kb-cli with `--config`, integration tests, TUI session)
/// already has a [`kebab_config::Config`] in hand. The public free
/// function [`ingest`] wraps this with the XDG-default load.
/// Config-explicit ingest entry point — bypasses [`load_config`] when
/// the caller (kebab-cli with `--config`, integration tests, TUI
/// session) already has a [`kebab_config::Config`] in hand.
///
/// This is the no-progress entry point retained for callers that
/// don't care about streaming progress (older tests, future code that
/// runs ingest as a one-shot). It forwards into
/// [`ingest_with_config_progress`] with `progress = None`.
#[doc(hidden)]
pub fn ingest_with_config(
config: kebab_config::Config,
scope: SourceScope,
summary_only: bool,
) -> anyhow::Result<IngestReport> {
ingest_with_config_progress(config, scope, summary_only, None)
}
/// Config + progress variant — same as [`ingest_with_config`] but the
/// caller may inject an `mpsc::Sender<IngestEvent>` to receive
/// streaming progress. CLI (`p9-fb-02`) feeds this into the
/// `ingest_progress.v1` line-delimited dump; TUI (`p9-fb-03`) feeds it
/// into the status-bar reducer; either may pass `None` to suppress
/// emission entirely. Send is best-effort — see [`ingest_progress`]
/// for the contract.
#[doc(hidden)]
pub fn ingest_with_config_progress(
config: kebab_config::Config,
scope: SourceScope,
summary_only: bool,
progress: Option<std::sync::mpsc::Sender<crate::ingest_progress::IngestEvent>>,
) -> anyhow::Result<IngestReport> {
ingest_with_config_cancellable(config, scope, summary_only, progress, None)
}
/// Config + opts variant (p9-fb-23). Supersedes the positional
/// `ingest_with_config_cancellable` fn; callers now pass an
/// [`IngestOpts`] struct so future knobs (e.g. `force_reingest`,
/// `dry_run`) land additively without churning every call site.
///
/// Existing callers that still pass positional `progress` + `cancel`
/// should use [`ingest_with_config_cancellable`], which remains as a
/// thin wrapper that builds `IngestOpts` and forwards here.
/// This is the orchestrator: all former intermediate variants
/// (`ingest_with_config_progress`, `ingest_with_config_cancellable`,
/// `ingest_with_config_opts`) are collapsed here. Pass progress /
/// cancel / force_reingest / summary_only through [`IngestOpts`].
///
/// Per design §10 (cancellation contract — unchanged from p9-fb-04):
///
@@ -261,10 +238,9 @@ pub fn ingest_with_config_progress(
/// CLI's `Ctrl-C` SIGINT handler and TUI's `Esc` / `Ctrl-C` both
/// flip the same `AtomicBool` (via `opts.cancel`).
#[doc(hidden)]
pub fn ingest_with_config_opts(
pub fn ingest_with_config(
config: kebab_config::Config,
scope: SourceScope,
summary_only: bool,
opts: IngestOpts,
) -> anyhow::Result<IngestReport> {
let progress = opts.progress.as_ref();
@@ -636,7 +612,7 @@ pub fn ingest_with_config_opts(
// ingest-specific aggregate counts row.
let payload = serde_json::json!({
"scope": scope,
"summary_only": summary_only,
"summary_only": opts.summary_only,
});
let job_id_res = <SqliteStoreAlias as kebab_core::JobRepo>::create(
&app.sqlite,
@@ -698,7 +674,7 @@ pub fn ingest_with_config_opts(
// the count columns are populated either way.
let scope_json = serde_json::to_string(&scope)
.context("kb-app::ingest: serialize scope for ingest_runs.scope_json")?;
let items_json: Option<String> = if summary_only {
let items_json: Option<String> = if opts.summary_only {
None
} else {
match serde_json::to_string(&items) {
@@ -830,39 +806,10 @@ pub fn ingest_with_config_opts(
skipped_size_exceeded: fs_skips.skipped_size_exceeded,
skip_examples: fs_skips.skip_examples,
purged_deleted_files,
items: if summary_only { None } else { Some(items) },
items: if opts.summary_only { None } else { Some(items) },
})
}
/// Config + progress + cancel variant (p9-fb-04). Retained as a thin
/// wrapper around [`ingest_with_config_opts`] for external callers
/// (test fixtures, CLI) that pass positional `progress` + `cancel`
/// arguments. New callers should prefer [`ingest_with_config_opts`]
/// with an explicit [`IngestOpts`].
///
/// CLI's `Ctrl-C` SIGINT handler and TUI's `Esc` / `Ctrl-C` both
/// flip the `cancel` `AtomicBool`. Pass `None` to retain
/// pre-p9-fb-04 behaviour (uncancellable).
#[doc(hidden)]
pub fn ingest_with_config_cancellable(
config: kebab_config::Config,
scope: SourceScope,
summary_only: bool,
progress: Option<std::sync::mpsc::Sender<crate::ingest_progress::IngestEvent>>,
cancel: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
) -> anyhow::Result<IngestReport> {
ingest_with_config_opts(
config,
scope,
summary_only,
IngestOpts {
progress,
cancel,
force_reingest: false,
},
)
}
/// Mint a stable 32-hex-char `run_id` for an `ingest_runs` row.
/// `(scope, started_at_nanos)` is enough to make two runs with the
/// same scope started a nanosecond apart distinguish — same shape as
@@ -3772,8 +3719,7 @@ pub fn ingest_file_with_config(
exclude: config.workspace.exclude.clone(),
};
let opts = IngestOpts::default();
ingest_with_config_opts(config, scope, /* summary_only = */ false, opts)
ingest_with_config(config, scope, IngestOpts::default())
}
/// Stdin ingest (p9-fb-31, v1 markdown only). Prepends a YAML

View File

@@ -21,7 +21,7 @@ use common::TestEnv;
#[ignore = "requires real Ollama on 127.0.0.1:11434"]
fn ask_lexical_smoke() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let opts = kebab_app::AskOpts {
k: 5,

View File

@@ -29,7 +29,7 @@ fn rust_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no errors expected: {report:?}");
@@ -128,7 +128,7 @@ fn rust_code_search_hit_has_repo() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
@@ -176,7 +176,7 @@ fn python_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert!(report.new >= 1, "python file ingested: {report:?}");
@@ -254,7 +254,7 @@ fn typescript_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert!(report.new >= 1, "ts file ingested: {report:?}");
@@ -332,7 +332,7 @@ fn javascript_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert!(report.new >= 1, "js file ingested: {report:?}");
@@ -410,7 +410,7 @@ fn go_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0);
assert!(report.new >= 1);
@@ -483,7 +483,7 @@ fn java_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0);
assert!(report.new >= 1);
@@ -560,7 +560,7 @@ fn kotlin_file_ingests_and_searches_as_code_citation() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0);
assert!(report.new >= 1);
@@ -635,7 +635,7 @@ fn tier2_k8s_yaml_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "yaml file ingested: {report:?}");
@@ -720,7 +720,7 @@ fn tier2_dockerfile_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "Dockerfile ingested: {report:?}");
@@ -805,7 +805,7 @@ fn tier2_cargo_toml_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "Cargo.toml ingested: {report:?}");
@@ -890,7 +890,7 @@ fn tier3_shell_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "shell file ingested: {report:?}");
@@ -979,7 +979,7 @@ fn tier3_yaml_fallback_picks_up_non_k8s_yaml() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(
@@ -1063,7 +1063,7 @@ fn rust_file_re_ingest_is_unchanged() {
std::fs::write(env.workspace_root.join("stable.rs"), "pub fn noop() {}\n").unwrap();
let r1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let r1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let item1 = r1
.items
.as_ref()
@@ -1074,7 +1074,7 @@ fn rust_file_re_ingest_is_unchanged() {
.unwrap();
assert_eq!(item1.kind, IngestItemKind::New);
let r2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let r2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let item2 = r2
.items
.unwrap()
@@ -1105,7 +1105,7 @@ fn tier3_yaml_fallback_reingest_is_unchanged() {
)
.unwrap();
let report1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("first ingest");
let item1 = report1
.items
@@ -1125,7 +1125,7 @@ fn tier3_yaml_fallback_reingest_is_unchanged() {
"first ingest must use Tier 3 fallback chunker"
);
let report2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("second ingest");
let item2 = report2
.items
@@ -1155,7 +1155,7 @@ fn tier1_c_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "c file ingested: {report:?}");
@@ -1241,7 +1241,7 @@ fn tier1_cpp_ingest_searchable() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors: {report:?}");
assert!(report.new >= 1, "cpp file ingested: {report:?}");
@@ -1333,7 +1333,7 @@ fn tier2_k8s_multi_resource_yaml_ingests_without_collision() {
)
.unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("ingest must succeed");
// The bug: this would land in report with an error + UNIQUE constraint message.
@@ -1389,7 +1389,7 @@ fn tier3_shell_reingest_is_unchanged() {
)
.unwrap();
let report1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("first ingest");
let item1 = report1
.items
@@ -1404,7 +1404,7 @@ fn tier3_shell_reingest_is_unchanged() {
item1.kind
);
let report2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
let report2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("second ingest");
let item2 = report2
.items

View File

@@ -107,7 +107,7 @@ pub fn ingest_md(env: &TestEnv, relative_path: &str, content: &str) {
std::fs::create_dir_all(parent).expect("create parent dirs");
}
std::fs::write(&path, content).expect("write workspace file");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest_with_config");
}

View File

@@ -15,7 +15,7 @@ mod common;
use common::TestEnv;
use kebab_app::{IngestOpts, ingest_with_config, ingest_with_config_opts};
use kebab_app::{IngestOpts, ingest_with_config};
use kebab_core::IngestItemKind;
/// Seed a workspace with a markdown + a rust file so both the markdown and
@@ -26,7 +26,7 @@ fn seed_and_first_ingest(env: &TestEnv) -> kebab_core::IngestReport {
"/// adds two integers\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
)
.unwrap();
let first = ingest_with_config(env.config.clone(), env.scope(), false).expect("first ingest");
let first = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).expect("first ingest");
assert_eq!(first.errors, 0, "first ingest must not error: {first:?}");
assert!(first.new >= 1, "first ingest creates docs: {first:?}");
assert_eq!(first.unchanged, 0, "first ingest has no unchanged: {first:?}");
@@ -34,7 +34,7 @@ fn seed_and_first_ingest(env: &TestEnv) -> kebab_core::IngestReport {
}
fn reingest(env: &TestEnv) -> kebab_core::IngestReport {
ingest_with_config_opts(env.config.clone(), env.scope(), false, IngestOpts::default())
ingest_with_config(env.config.clone(), env.scope(), IngestOpts::default())
.expect("re-ingest")
}

View File

@@ -16,8 +16,7 @@
mod common;
use common::TestEnv;
use kebab_app::IngestOpts;
use kebab_app::ingest_with_config_opts;
use kebab_app::{IngestOpts, ingest_with_config};
use kebab_core::{DocFilter, DocumentStore, SearchMode, SearchQuery, SourceScope};
/// Helper: open the store via `TestEnv` and run `list_documents`.
@@ -44,10 +43,9 @@ fn file_deletion_auto_purge() {
std::fs::write(&b_path, "// file b\nfn bravo() {}\n").unwrap();
// First ingest — both must be New.
let first = ingest_with_config_opts(
let first = ingest_with_config(
env.config.clone(),
env.scope(),
false,
IngestOpts::default(),
)
.expect("first ingest must succeed");
@@ -64,10 +62,9 @@ fn file_deletion_auto_purge() {
std::fs::remove_file(&b_path).expect("remove b.rs");
// Second ingest — scanned count drops by 1; b.rs should be purged.
let second = ingest_with_config_opts(
let second = ingest_with_config(
env.config.clone(),
env.scope(),
false,
IngestOpts::default(),
)
.expect("second ingest must succeed");
@@ -126,7 +123,7 @@ fn include_scope_narrowing_does_not_purge() {
exclude: env.config.workspace.exclude.clone(),
};
let first =
ingest_with_config_opts(env.config.clone(), wide_scope, false, IngestOpts::default())
ingest_with_config(env.config.clone(), wide_scope, IngestOpts::default())
.expect("first ingest (wide) must succeed");
assert!(first.new >= 2, "expected at least 2 new docs: {first:?}");
assert_eq!(
@@ -141,10 +138,9 @@ fn include_scope_narrowing_does_not_purge() {
include: vec!["a_narrow.rs".to_string()],
exclude: env.config.workspace.exclude.clone(),
};
let second = ingest_with_config_opts(
let second = ingest_with_config(
env.config.clone(),
narrow_scope,
false,
IngestOpts::default(),
)
.expect("second ingest (narrow) must succeed");

View File

@@ -71,7 +71,7 @@ async fn ingest_image_with_ocr_produces_chunk_containing_ocr_text() {
let env_scope = env.scope();
let report = spawn_blocking(move || {
kebab_app::ingest_with_config(cfg_clone, env_scope, false)
kebab_app::ingest_with_config(cfg_clone, env_scope, kebab_app::IngestOpts::default())
.expect("image ingest must succeed")
})
.await
@@ -167,7 +167,7 @@ async fn ingest_image_with_ocr_and_caption_populates_both_fields() {
let cfg_clone = cfg.clone();
let scope = env.scope();
let report = spawn_blocking(move || {
kebab_app::ingest_with_config(cfg_clone, scope, false)
kebab_app::ingest_with_config(cfg_clone, scope, kebab_app::IngestOpts::default())
.expect("ingest must succeed with both OCR+caption")
})
.await
@@ -212,7 +212,7 @@ async fn ocr_failure_indexes_asset_with_warning_no_error_counter() {
let cfg_clone = cfg.clone();
let scope = env.scope();
let report = spawn_blocking(move || {
kebab_app::ingest_with_config(cfg_clone, scope, false)
kebab_app::ingest_with_config(cfg_clone, scope, kebab_app::IngestOpts::default())
.expect("ingest does not abort on lenient OCR failure")
})
.await
@@ -276,7 +276,7 @@ async fn image_indexed_with_filename_when_ocr_and_caption_disabled() {
let cfg_clone = cfg.clone();
let scope = env.scope();
let report = spawn_blocking(move || {
kebab_app::ingest_with_config(cfg_clone, scope, false).expect("ingest with no OCR/caption")
kebab_app::ingest_with_config(cfg_clone, scope, kebab_app::IngestOpts::default()).expect("ingest with no OCR/caption")
})
.await
.expect("task");
@@ -340,7 +340,7 @@ async fn garbage_png_increments_errors_counter_exactly_once() {
let cfg_clone = cfg.clone();
let scope = env.scope();
let report = spawn_blocking(move || {
kebab_app::ingest_with_config(cfg_clone, scope, false)
kebab_app::ingest_with_config(cfg_clone, scope, kebab_app::IngestOpts::default())
.expect("ingest does not abort on per-asset failure")
})
.await
@@ -399,10 +399,10 @@ async fn re_ingest_image_produces_unchanged_with_same_doc_id() {
let scope1 = scope.clone();
let scope2 = scope.clone();
let r1 = spawn_blocking(move || kebab_app::ingest_with_config(cfg1, scope1, false).unwrap())
let r1 = spawn_blocking(move || kebab_app::ingest_with_config(cfg1, scope1, kebab_app::IngestOpts::default()).unwrap())
.await
.unwrap();
let r2 = spawn_blocking(move || kebab_app::ingest_with_config(cfg2, scope2, false).unwrap())
let r2 = spawn_blocking(move || kebab_app::ingest_with_config(cfg2, scope2, kebab_app::IngestOpts::default()).unwrap())
.await
.unwrap();

View File

@@ -12,7 +12,7 @@ mod common;
use common::TestEnv;
use kebab_app::{IngestOpts, ingest_with_config, ingest_with_config_opts};
use kebab_app::{IngestOpts, ingest_with_config};
#[test]
fn second_ingest_of_unchanged_corpus_marks_all_unchanged() {
@@ -21,7 +21,7 @@ fn second_ingest_of_unchanged_corpus_marks_all_unchanged() {
// First ingest — populates the DB. Use the legacy entry so the
// assertions cover the "previously ingested" set without needing
// IngestOpts::default() to behave identically.
let first = ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let first = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(first.errors, 0, "first ingest must not error: {first:?}");
assert!(
first.new >= 1,
@@ -36,13 +36,8 @@ fn second_ingest_of_unchanged_corpus_marks_all_unchanged() {
// Second ingest — same files, same versions → all assets must be
// labelled Unchanged (no parse / chunk / embed re-work).
let second = ingest_with_config_opts(
env.config.clone(),
env.scope(),
false,
IngestOpts::default(),
)
.unwrap();
let second = ingest_with_config(env.config.clone(), env.scope(), IngestOpts::default())
.unwrap();
assert_eq!(
second.scanned, scanned,
"second scanned matches first: {second:?}"
@@ -63,7 +58,7 @@ fn second_ingest_of_unchanged_corpus_marks_all_unchanged() {
fn force_reingest_bypasses_skip() {
let env = TestEnv::lexical_only();
let first = ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let first = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(first.errors, 0, "first ingest must not error: {first:?}");
assert!(
first.new >= 1,
@@ -71,10 +66,9 @@ fn force_reingest_bypasses_skip() {
);
let scanned = first.scanned;
let second = ingest_with_config_opts(
let second = ingest_with_config(
env.config.clone(),
env.scope(),
false,
IngestOpts {
force_reingest: true,
..Default::default()

View File

@@ -1,4 +1,4 @@
//! Integration coverage for `ingest_with_config_cancellable`
//! Integration coverage for cancellable ingest via `IngestOpts`
//! (p9-fb-04). Asserts the §10 invariants:
//!
//! - Cancel set BEFORE the loop starts → no asset is processed.
@@ -21,12 +21,15 @@ fn run_with(
cancel: Arc<AtomicBool>,
progress: Option<mpsc::Sender<IngestEvent>>,
) -> kebab_core::IngestReport {
kebab_app::ingest_with_config_cancellable(
kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
true,
progress,
Some(cancel),
kebab_app::IngestOpts {
progress,
cancel: Some(cancel),
summary_only: true,
..Default::default()
},
)
.unwrap()
}
@@ -89,7 +92,15 @@ fn cancel_mid_loop_after_first_asset_keeps_idempotent_resume() {
assert!(report.new < 3, "loop should have broken: {report:?}");
// Idempotent re-ingest finishes the job.
let r2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let r2 = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
kebab_app::IngestOpts {
summary_only: true,
..Default::default()
},
)
.unwrap();
assert_eq!(r2.scanned, 3, "re-scan: {r2:?}");
// Total committed across both runs covers all 3 docs (some New
// first run, rest New on second; or first run was 0 → all New on
@@ -108,7 +119,15 @@ fn cancel_none_is_uncancellable_default() {
let env = TestEnv::lexical_only();
let (tx, rx) = mpsc::channel::<IngestEvent>();
let report =
kebab_app::ingest_with_config_progress(env.config.clone(), env.scope(), true, Some(tx))
kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
kebab_app::IngestOpts {
progress: Some(tx),
summary_only: true,
..Default::default()
},
)
.unwrap();
assert_eq!(report.scanned, 3);
assert_eq!(report.new, 3);

View File

@@ -8,7 +8,7 @@ use common::TestEnv;
#[test]
fn ingest_then_list_inspects_round_trip() {
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
// The fixture has 3 markdown files; first ingest should label them
// all as New.
@@ -42,10 +42,10 @@ fn ingest_then_list_inspects_round_trip() {
fn ingest_idempotent_on_second_run() {
let env = TestEnv::lexical_only();
let r1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let r1 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(r1.new, 3);
let r2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let r2 = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
// Same files re-ingested — p9-fb-23 task 7 introduced the early-skip
// path: when checksum + parser/chunker/embedding versions all match,
// the second run reports `Unchanged` rather than `Updated`. Pre-p9-fb-23
@@ -66,7 +66,7 @@ fn ingest_idempotent_on_second_run() {
#[test]
fn ingest_summary_only_drops_items() {
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
assert_eq!(report.scanned, 3);
assert!(report.items.is_none(), "summary-only should null items");
}
@@ -78,7 +78,7 @@ fn ingest_records_ingest_runs_row_with_aggregate_counts() {
// of every run. `summary_only=true` writes `items_json=NULL`; the
// counts MUST still be present.
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
assert_eq!(report.scanned, 3);
let db_path = std::path::PathBuf::from(&env.config.storage.data_dir).join("kebab.sqlite");
@@ -130,7 +130,7 @@ fn ingest_provider_none_skips_lance() {
// tree shape (no `<data_dir>/lancedb` directory, or no `*.lance`
// tables under it).
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(report.errors, 0, "lexical-only run must not error");
assert_eq!(report.new, 3);
@@ -157,7 +157,7 @@ fn ingest_provider_none_skips_lance() {
#[test]
fn list_docs_filters_by_tags_any() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let filter = kebab_core::DocFilter {
tags_any: vec!["python".to_string()],
@@ -205,16 +205,14 @@ fn inspect_chunk_not_found_returns_actionable_error() {
assert!(msg.contains("not found"), "got: {msg}");
}
/// p9-fb-23 task 6: `ingest_with_config_opts` with `IngestOpts::default()`
/// must behave identically to `ingest_with_config` — first ingest reports
/// all assets as new, no errors, no unchanged.
/// p9-fb-23 task 6: `ingest_with_config` with `IngestOpts::default()`
/// must report all assets as new, no errors, no unchanged on first ingest.
#[test]
fn ingest_with_config_opts_default_matches_legacy_behaviour() {
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config_opts(
let report = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
false,
kebab_app::IngestOpts::default(),
)
.unwrap();
@@ -232,7 +230,7 @@ fn ingest_with_config_opts_default_matches_legacy_behaviour() {
#[test]
fn ingest_stamps_chunker_version_on_document() {
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert!(report.new >= 1, "expected at least one new doc: {report:?}");
assert_eq!(report.errors, 0, "no errors expected: {report:?}");

View File

@@ -4,7 +4,7 @@
use std::path::PathBuf;
use kebab_app::{IngestOpts, ingest_with_config_opts};
use kebab_app::{IngestOpts, ingest_with_config};
use kebab_config::{Config, LoggingCfg};
use kebab_core::SourceScope;
use serde_json::Value;
@@ -61,7 +61,7 @@ fn ingest_log_smoke() {
};
// 3. Run ingest.
ingest_with_config_opts(cfg, scope, false, IngestOpts::default())
ingest_with_config(cfg, scope, IngestOpts::default())
.expect("ingest should succeed");
// 4. Assert log file exists in log_dir.
@@ -148,7 +148,7 @@ fn ingest_log_disabled_emits_no_file() {
..Default::default()
};
ingest_with_config_opts(cfg, scope, false, IngestOpts::default())
ingest_with_config(cfg, scope, IngestOpts::default())
.expect("ingest should succeed");
// log_dir should either not exist or contain 0 ingest-*.ndjson files.

View File

@@ -44,7 +44,7 @@ fn ingest_with_mock_ocr_yields_pdf_ocr_summary() {
let env = make_ocr_env_real();
let report =
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).expect("ingest");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).expect("ingest");
assert!(report.new >= 1, "at least one PDF ingested: {report:?}");
@@ -72,7 +72,7 @@ fn ingest_with_mock_ocr_yields_pdf_ocr_summary() {
fn ocr_text_indexed_and_searchable() {
let env = make_ocr_env_real();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).expect("ingest");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).expect("ingest");
// Search for a Korean morpheme expected to appear in qwen2.5vl:3b OCR
// output of the PoC ground-truth page. "다음" is a high-frequency token
@@ -105,12 +105,13 @@ fn ingest_with_cancel_aborts_mid_pdf() {
let cancel = Arc::new(AtomicBool::new(true)); // pre-set — abort immediately
let result = kebab_app::ingest_with_config_cancellable(
let result = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
false,
None,
Some(cancel),
kebab_app::IngestOpts {
cancel: Some(cancel),
..Default::default()
},
);
// Both Ok (pre-cancel exit) and Err (eager OCR engine fail) are acceptable —
// key assertion is no panic/deadlock.

View File

@@ -1,4 +1,4 @@
//! Integration coverage for `ingest_with_config_progress` —
//! Integration coverage for streaming ingest progress via `IngestOpts` —
//! exercises the streaming progress channel against the same lexical
//! fixture used by `ingest_lexical.rs`.
@@ -13,14 +13,19 @@ use kebab_core::IngestItemKind;
fn run_with_progress() -> Vec<IngestEvent> {
let env = TestEnv::lexical_only();
let (tx, rx) = mpsc::channel::<IngestEvent>();
let report =
kebab_app::ingest_with_config_progress(env.config.clone(), env.scope(), false, Some(tx))
.unwrap();
let report = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
kebab_app::IngestOpts {
progress: Some(tx),
..Default::default()
},
)
.unwrap();
assert_eq!(report.scanned, 3);
assert_eq!(report.new, 3);
// Drain until the sender (held inside `ingest_with_config_progress`)
// is dropped on return.
// Drain until the sender (held inside ingest_with_config) is dropped on return.
let mut events = Vec::new();
while let Ok(ev) = rx.recv() {
events.push(ev);
@@ -142,13 +147,18 @@ fn progress_event_sequence_matches_design_section_2_4a() {
#[test]
fn ingest_with_config_progress_none_matches_ingest_with_config() {
// Forwarding wrapper: `ingest_with_config(...)` and
// `ingest_with_config_progress(..., None)` must produce identical
// reports modulo wall-clock duration.
// `ingest_with_config(...)` with no progress must produce identical
// reports to a call with progress=None — modulo wall-clock duration.
let env = TestEnv::lexical_only();
let r_none =
kebab_app::ingest_with_config_progress(env.config.clone(), env.scope(), true, None)
.unwrap();
let r_none = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
kebab_app::IngestOpts {
summary_only: true,
..Default::default()
},
)
.unwrap();
assert_eq!(r_none.scanned, 3);
assert_eq!(r_none.new, 3);
}
@@ -160,9 +170,16 @@ fn dropped_receiver_does_not_panic_or_fail_ingest() {
let env = TestEnv::lexical_only();
let (tx, rx) = mpsc::channel::<IngestEvent>();
drop(rx);
let report =
kebab_app::ingest_with_config_progress(env.config.clone(), env.scope(), true, Some(tx))
.unwrap();
let report = kebab_app::ingest_with_config(
env.config.clone(),
env.scope(),
kebab_app::IngestOpts {
progress: Some(tx),
summary_only: true,
..Default::default()
},
)
.unwrap();
assert_eq!(report.scanned, 3);
}
@@ -208,8 +225,15 @@ fn pdf_ocr_progress_emits_started_finished_events() {
};
let (tx, rx) = mpsc::channel::<IngestEvent>();
let _report = kebab_app::ingest_with_config_progress(config, scope, false, Some(tx))
.expect("ingest_with_config_progress");
let _report = kebab_app::ingest_with_config(
config,
scope,
kebab_app::IngestOpts {
progress: Some(tx),
..Default::default()
},
)
.expect("ingest_with_config");
let events: Vec<_> = rx.iter().collect();

View File

@@ -66,7 +66,7 @@ async fn ingest_dual_write_doc_id_matches_ndjson() {
std::fs::copy(scanned_pdf_src(), &dest).expect("copy scanned PDF");
// Run ingest
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).expect("ingest");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).expect("ingest");
// Read ndjson log
let log_files: Vec<_> = std::fs::read_dir(&log_dir)

View File

@@ -141,7 +141,7 @@ fn ingest_3_page_pdf_produces_one_doc_and_per_page_chunks() {
write_pdf(&env.workspace_root, "three.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false)
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("PDF ingest must succeed");
assert_eq!(report.errors, 0);
@@ -203,7 +203,7 @@ fn re_ingest_identical_pdf_produces_unchanged_with_same_doc_id() {
write_pdf(&env.workspace_root, "stable.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report1 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report1 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let item1 = report1
.items
.as_ref()
@@ -214,7 +214,7 @@ fn re_ingest_identical_pdf_produces_unchanged_with_same_doc_id() {
.unwrap();
assert_eq!(item1.kind, IngestItemKind::New);
let report2 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report2 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let item2 = report2
.items
.unwrap()
@@ -238,7 +238,7 @@ fn re_ingest_edited_pdf_produces_new_doc_id() {
std::fs::write(&path, &bytes_v1).unwrap();
let cfg = cfg_with_pdf(&env);
let report_v1 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report_v1 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let id_v1 = report_v1
.items
.as_ref()
@@ -253,7 +253,7 @@ fn re_ingest_edited_pdf_produces_new_doc_id() {
let bytes_v2 = build_text_pdf(&[Some("VERSION TWO entirely different body content.")]);
std::fs::write(&path, &bytes_v2).unwrap();
let report_v2 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report_v2 = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let item_v2 = report_v2
.items
.as_ref()
@@ -278,7 +278,7 @@ fn encrypted_pdf_fails_with_qpdf_hint() {
write_pdf(&env.workspace_root, "secret.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg, env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg, env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(
report.errors, 1,
"encrypted PDF must increment errors exactly once"
@@ -308,7 +308,7 @@ fn corrupt_pdf_fails_without_storing() {
write_pdf(&env.workspace_root, "corrupt.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(
report.errors, 1,
"corrupt PDF must increment errors exactly once"
@@ -342,7 +342,7 @@ fn mixed_page_pdf_stores_asset_with_scanned_candidate_warning() {
write_pdf(&env.workspace_root, "mixed.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(
report.errors, 0,
"scanned candidate is a Warning, not Error"
@@ -413,7 +413,7 @@ fn ingest_report_arithmetic_invariant_holds_with_corrupt_pdf() {
write_pdf(&env.workspace_root, "broken.pdf", &corrupt_pdf());
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg, env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg, env.scope(), kebab_app::IngestOpts::default()).unwrap();
let total = report.new + report.updated + report.skipped + report.errors;
assert_eq!(
report.scanned, total,
@@ -439,7 +439,7 @@ fn long_pdf_round_trips_through_lexical_pipeline() {
write_pdf(&env.workspace_root, "long.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
assert_eq!(report.errors, 0);
let pdf_item = report
.items
@@ -470,7 +470,7 @@ fn inspect_doc_surfaces_page_spans() {
write_pdf(&env.workspace_root, "inspect.pdf", &bytes);
let cfg = cfg_with_pdf(&env);
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(cfg.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let pdf_item = report
.items
.as_ref()

View File

@@ -16,7 +16,7 @@
mod common;
use common::TestEnv;
use kebab_app::IngestOpts;
use kebab_app::{IngestOpts, ingest_with_config};
use kebab_app::reset::{ResetScope, execute};
use kebab_core::{DocFilter, DocumentStore, SourceScope};
@@ -51,13 +51,8 @@ fn reset_orphans_only_purges_out_of_scope_docs() {
include: vec!["**/*.rs".to_string()],
exclude: env.config.workspace.exclude.clone(),
};
let first = kebab_app::ingest_with_config_opts(
env.config.clone(),
wide_scope,
false,
IngestOpts::default(),
)
.expect("first ingest must succeed");
let first = ingest_with_config(env.config.clone(), wide_scope, IngestOpts::default())
.expect("first ingest must succeed");
// The fixture workspace may contain other .rs files — just assert we
// got at least 3 new docs (our a.rs, b.rs, c.rs).
assert!(first.new >= 3, "expected at least 3 new docs: {first:?}");

View File

@@ -58,7 +58,7 @@ fn schema_emits_active_parsers_and_chunkers_array_after_ingest() {
let cfg = minimal_config(dir.path(), &workspace);
let scope = minimal_scope(&workspace);
kebab_app::ingest_with_config(cfg.clone(), scope, false).unwrap();
kebab_app::ingest_with_config(cfg.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let s = schema_with_config(&cfg).unwrap();
assert!(

View File

@@ -40,7 +40,7 @@ fn schema_report_reflects_freshly_ingested_kb() {
let config = minimal_config(&data_dir, &workspace_root);
let _report =
kebab_app::ingest_with_config(config.clone(), minimal_scope(&workspace_root), false)
kebab_app::ingest_with_config(config.clone(), minimal_scope(&workspace_root), kebab_app::IngestOpts::default())
.unwrap();
let schema = kebab_app::schema_with_config(&config).unwrap();
@@ -100,7 +100,7 @@ fn schema_report_on_empty_kb_has_zero_counts() {
// Run ingest over the empty workspace — creates kebab.sqlite, runs
// migrations, records 0 docs. schema_with_config can then open_existing.
let report =
kebab_app::ingest_with_config(config.clone(), minimal_scope(&workspace_root), false)
kebab_app::ingest_with_config(config.clone(), minimal_scope(&workspace_root), kebab_app::IngestOpts::default())
.unwrap();
assert_eq!(report.new, 0, "empty workspace should yield 0 new docs");

View File

@@ -24,7 +24,7 @@ fn korean_lexical_query_returns_korean_document() {
.expect("write Korean fixture doc");
// Ingest — lexical_only() disables fastembed so no AVX required.
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest must succeed");
// Lexical search for "러스트" — must return the Korean document.
@@ -72,7 +72,7 @@ fn lexical_multi_token_korean_query_hits() {
.join("hash-table.md");
std::fs::copy(&src, &dest).expect("copy korean fixture");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest must succeed");
let hits =
@@ -108,7 +108,7 @@ fn lexical_mixed_korean_english_multi_token_query_hits() {
)
.expect("write rust-hash fixture");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest must succeed");
let hits =
@@ -144,7 +144,7 @@ fn korean_morphological_2char_query_lexical_mode() {
)
.expect("write korean-wiki fixture");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest must succeed");
let hits = kebab_app::search_with_config(env.config.clone(), common::lexical_query("한국"))
@@ -176,7 +176,7 @@ fn korean_morphological_mixed_english_korean_query() {
)
.expect("write rust-optimization fixture");
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true)
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() })
.expect("ingest must succeed");
let hits = kebab_app::search_with_config(env.config.clone(), common::lexical_query("Rust"))

View File

@@ -8,7 +8,7 @@ use common::TestEnv;
#[test]
fn lexical_search_returns_hits_after_ingest() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
// "Ownership" appears as a heading + paragraph in intro.md and
// matches FTS5 default tokenizer easily.
@@ -34,7 +34,7 @@ fn lexical_search_returns_hits_after_ingest() {
#[test]
fn lexical_search_empty_query_returns_empty() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let hits =
kebab_app::search_with_config(env.config.clone(), common::lexical_query(" ")).unwrap();
assert!(hits.is_empty(), "blank query must short-circuit empty");
@@ -46,7 +46,7 @@ fn lexical_search_empty_query_returns_empty() {
#[test]
fn cached_search_returns_same_hits_on_repeat() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let app = kebab_app::App::open_with_config(env.config.clone()).unwrap();
let first = app.search(common::lexical_query("ownership")).unwrap();
assert!(!first.is_empty(), "first call must return ≥1 hit");
@@ -68,7 +68,7 @@ fn cached_search_returns_same_hits_on_repeat() {
#[test]
fn cache_key_normalization_treats_case_and_whitespace_as_equivalent() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let app = kebab_app::App::open_with_config(env.config.clone()).unwrap();
let plain = app.search(common::lexical_query("ownership")).unwrap();
let upper = app.search(common::lexical_query("OWNERSHIP")).unwrap();
@@ -86,7 +86,7 @@ fn cache_key_normalization_treats_case_and_whitespace_as_equivalent() {
#[test]
fn search_uncached_returns_same_hits_as_cached() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let cached =
kebab_app::search_with_config(env.config.clone(), common::lexical_query("ownership"))
.unwrap();
@@ -116,7 +116,7 @@ fn first_ingest_bumps_corpus_revision() {
let baseline = store_before.corpus_revision();
assert_eq!(baseline, 3, "fresh store post-V011 baseline = 3");
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
assert!(
report.new + report.updated > 0,
"first ingest must commit ≥1 doc"
@@ -133,7 +133,7 @@ fn first_ingest_bumps_corpus_revision() {
#[test]
fn vector_mode_with_provider_none_errors_clearly() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let q = kebab_core::SearchQuery {
text: "ownership".to_string(),

View File

@@ -21,7 +21,7 @@ fn lexical_query_owner() -> kebab_core::SearchQuery {
#[test]
fn fresh_doc_is_not_stale_with_default_threshold() {
let env = TestEnv::lexical_only();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
let app = kebab_app::App::open_with_config(env.config.clone()).unwrap();
let hits = app.search(lexical_query_owner()).unwrap();
@@ -43,7 +43,7 @@ fn threshold_zero_disables_staleness() {
let mut env = TestEnv::lexical_only();
env.config.search.stale_threshold_days = 0;
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
common::backdate_document_updated_at(&env, "intro.md", 365);
let app = kebab_app::App::open_with_config(env.config.clone()).unwrap();
@@ -66,7 +66,7 @@ fn old_doc_marked_stale() {
let mut env = TestEnv::lexical_only();
env.config.search.stale_threshold_days = 30;
kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
common::backdate_document_updated_at(&env, "intro.md", 60);
let app = kebab_app::App::open_with_config(env.config.clone()).unwrap();

View File

@@ -29,7 +29,7 @@ fn ingest_then_hybrid_search_returns_hits() {
require_avx_or_panic();
let env = TestEnv::with_embeddings();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
assert_eq!(report.errors, 0, "no per-file errors: {report:?}");
assert_eq!(report.new, 3);
@@ -55,7 +55,7 @@ fn ingest_then_vector_search_carries_embedding_model() {
require_avx_or_panic();
let env = TestEnv::with_embeddings();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), true).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts { summary_only: true, ..Default::default() }).unwrap();
assert_eq!(report.errors, 0, "no per-file errors: {report:?}");
assert_eq!(report.new, 3);

View File

@@ -13,7 +13,7 @@ fn unsupported_extension_skip_carries_warning_and_is_aggregated() {
std::fs::write(workspace_root.join("legacy.docx"), b"unsupported").unwrap();
std::fs::write(workspace_root.join("Makefile"), b"unsupported").unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false).unwrap();
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).unwrap();
let items = report.items.as_ref().expect("items array populated");
let docx_item = items

View File

@@ -45,7 +45,7 @@ fn twin_files_fetch_span_uses_correct_asset() {
// Ingest all files (fixture workspace + our two new twins).
let report =
ingest_with_config(env.config.clone(), env.scope(), false).expect("ingest must succeed");
ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default()).expect("ingest must succeed");
assert_eq!(report.errors, 0, "no ingest errors; report={report:?}");
// Both twin paths must appear as New in the report.
@@ -146,7 +146,7 @@ fn twin_files_fetch_span_uses_correct_asset() {
// re-check. Pre-fix this was the scenario that triggered the bug:
// after the second ingest the asset row's workspace_path could point
// at either twin, making one twin's span fetch behave incorrectly.
let report2 = ingest_with_config(env.config.clone(), env.scope(), false)
let report2 = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("second ingest must succeed");
assert_eq!(
report2.errors, 0,

View File

@@ -36,7 +36,7 @@ fn twin_files_second_ingest_is_unchanged() {
std::fs::write(pkg_b.join("__init__.py"), content).unwrap();
// First ingest — both files must be New.
let first = ingest_with_config(env.config.clone(), env.scope(), false)
let first = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("first ingest must succeed");
assert_eq!(first.errors, 0, "first ingest: no errors; report={first:?}");
@@ -59,7 +59,7 @@ fn twin_files_second_ingest_is_unchanged() {
}
// Second ingest — same files, same content → both must be Unchanged.
let second = ingest_with_config(env.config.clone(), env.scope(), false)
let second = ingest_with_config(env.config.clone(), env.scope(), kebab_app::IngestOpts::default())
.expect("second ingest must succeed");
assert_eq!(
second.errors, 0,

View File

@@ -1,6 +1,6 @@
//! `kebab ingest` SIGINT (Ctrl-C) handler — flips a shared
//! `Arc<AtomicBool>` so `kebab_app::ingest_with_config_cancellable`
//! can break at the next step boundary.
//! `Arc<AtomicBool>` so `kebab_app::ingest_with_config`
//! can break at the next step boundary (via `IngestOpts::cancel`).
//!
//! Per spec §10: the second Ctrl-C is a hard exit (130 = SIGINT
//! conventional). We count signal arrivals via a private atomic and
@@ -25,8 +25,8 @@ use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
/// Install a SIGINT handler that:
/// - on first signal: sets `cancel.store(true)` so the cooperative
/// cancel loop in `kebab_app::ingest_with_config_cancellable`
/// breaks at its next step boundary.
/// cancel loop in `kebab_app::ingest_with_config` (via
/// `IngestOpts::cancel`) breaks at its next step boundary.
/// - on second signal: hard-exits with code 130 (SIGINT
/// convention).
///

View File

@@ -672,14 +672,14 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
// p9-fb-23: use IngestOpts so force_reingest threads through
// without churning the positional-arg list.
let ingest_result = kebab_app::ingest_with_config_opts(
let ingest_result = kebab_app::ingest_with_config(
cfg,
scope,
*summary_only,
kebab_app::IngestOpts {
progress: Some(tx),
cancel: Some(cancel_token),
force_reingest: *force_reingest,
summary_only: *summary_only,
},
);

View File

@@ -16,7 +16,7 @@
//! Each subprocess of the binary creates one `ProgressDisplay` and
//! drives it from a background thread that drains an
//! `mpsc::Receiver<IngestEvent>`. The thread terminates when the
//! `Sender` end is dropped (i.e. when `ingest_with_config_progress`
//! `Sender` end is dropped (i.e. when `ingest_with_config`
//! returns).
use std::collections::HashMap;

View File

@@ -33,7 +33,7 @@ async fn ask_tool_returns_answer_v1_with_refusal_on_empty_kb() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(cfg, None);
let handler = KebabHandler::new(state);

View File

@@ -90,7 +90,7 @@ async fn ask_tool_routes_multi_hop_true_to_decompose_first() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(cfg, None);
let handler = KebabHandler::new(state);
@@ -177,7 +177,7 @@ async fn ask_tool_multi_hop_short_circuits_when_probe_empty() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(cfg.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(cfg.clone(), None);
let handler = KebabHandler::new(state);

View File

@@ -36,7 +36,7 @@ fn setup() -> (tempfile::TempDir, KebabHandler) {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);
(dir, handler)

View File

@@ -44,7 +44,7 @@ async fn fetch_tool_chunk_returns_fetch_result_v1() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);

View File

@@ -34,7 +34,7 @@ async fn schema_tool_returns_schema_v1_json() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);

View File

@@ -41,7 +41,7 @@ async fn search_tool_returns_search_response_v1() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);
@@ -141,7 +141,7 @@ async fn search_with_doc_id_filter_returns_only_target() {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);

View File

@@ -35,7 +35,7 @@ fn setup() -> (tempfile::TempDir, KebabHandler) {
include: vec![],
exclude: vec![],
};
let _ = kebab_app::ingest_with_config(config.clone(), scope, false).unwrap();
let _ = kebab_app::ingest_with_config(config.clone(), scope, kebab_app::IngestOpts::default()).unwrap();
let state = KebabAppState::new(config, None);
let handler = KebabHandler::new(state);
(dir, handler)