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:
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user