refactor(kebab-app): p9-fb-23 task 6 — IngestOpts struct + ingest_with_config_opts entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 18:04:50 +00:00
parent a16e9c9215
commit 4874304d5d
2 changed files with 85 additions and 13 deletions

View File

@@ -186,6 +186,22 @@ 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.
#[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.
pub force_reingest: bool,
}
pub fn ingest(scope: SourceScope, summary_only: bool) -> anyhow::Result<IngestReport> {
let config = load_config()?;
ingest_with_config(config, scope, summary_only)
@@ -226,12 +242,16 @@ pub fn ingest_with_config_progress(
ingest_with_config_cancellable(config, scope, summary_only, progress, None)
}
/// Config + progress + cancel variant (p9-fb-04). The caller injects
/// an `Arc<AtomicBool>` cancel token; setting it to `true` causes the
/// ingest loop to break at the next step boundary (asset loop iter
/// start), emit `IngestEvent::Aborted { counts: <partial> }`, and
/// return `Ok(IngestReport)` with whatever assets were committed
/// before cancellation. Per design §10:
/// 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.
///
/// Per design §10 (cancellation contract — unchanged from p9-fb-04):
///
/// - The current in-flight asset finishes (rollback would break
/// idempotent re-run). Subsequent assets are skipped.
@@ -242,23 +262,25 @@ pub fn ingest_with_config_progress(
/// doc_id recipes).
///
/// CLI's `Ctrl-C` SIGINT handler and TUI's `Esc` / `Ctrl-C` both
/// flip the same `AtomicBool`. Pass `None` to retain pre-p9-fb-04
/// behaviour (uncancellable).
/// flip the same `AtomicBool` (via `opts.cancel`).
#[doc(hidden)]
pub fn ingest_with_config_cancellable(
pub fn ingest_with_config_opts(
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>>,
opts: IngestOpts,
) -> anyhow::Result<IngestReport> {
let progress = progress.as_ref();
let progress = opts.progress.as_ref();
let cancelled = || {
cancel
opts.cancel
.as_ref()
.map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
.unwrap_or(false)
};
// p9-fb-23: opts.force_reingest is consumed by Task 7's skip-detection
// block. For Task 6 alone, the field is plumbed but unused — silence
// the warning until Task 7 wires it.
let _ = opts.force_reingest;
let started_instant = std::time::Instant::now();
let app = App::open_with_config(config)?;
@@ -638,6 +660,35 @@ pub fn ingest_with_config_cancellable(
})
}
/// 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

View File

@@ -219,6 +219,27 @@ 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.
#[test]
fn ingest_with_config_opts_default_matches_legacy_behaviour() {
let env = TestEnv::lexical_only();
let report = kebab_app::ingest_with_config_opts(
env.config.clone(),
env.scope(),
false,
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:?}");
assert_eq!(
report.unchanged, 0,
"first ingest cannot have unchanged: {report:?}"
);
}
/// p9-fb-23 task 5: every freshly-ingested markdown doc must carry
/// `last_chunker_version`. With `provider="none"` (lexical-only),
/// `last_embedding_version` stays `None`.