refactor(search): 죽은 search-cache + search --explain scaffold 제거
in-process LRU search cache 는 spine 재작성(#214)에서 이미 삭제됐는데 그 비계(scaffold)만 남아 있었다. 잔존물 제거: - App::search/search_uncached 붕괴 (search() 는 1줄 위임자였음) - search_uncached_with_config facade + 그 테스트 삭제 - 죽은 `search --no-cache` / `search --explain` CLI 플래그 제거 (ask --explain 은 live — 건드리지 않음) - 안 읽히던 RagCfg.explain_default config 필드 + fixture 제거 - 제거된 표면을 가리키던 stale 주석/문서(citation_helper/hybrid/ search·app-facade README/DOGFOOD/HANDOFF) 정정 코어 검색 출력은 byte-identical (search_uncached 본문이 search() 로 verbatim 이동). `search_cache: false` wire capability 는 유지 — 제거 시 schema.v1 breaking bump 이라 손해. 적대적 검증 2렌즈(correctness-preserved + wire/config-safe) 통과, dead-symbol-complete 가 잡은 5건 주석/문서 정정 완료. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
This commit is contained in:
@@ -80,7 +80,7 @@ P0~P5 직렬. P6~P9 P5 이후 병렬 가능.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-07)** — Markdown title fallback chain. `kebab-normalize::derive_title(frontmatter_title, &[Block], file_stem)` — 1) frontmatter title → 2) 첫 H1 → 3) 첫 H2 → 4) 첫 paragraph 80 chars → 5) 파일 stem (모든 단계 NFC 정규화, 빈 문자열 절대 반환 안 함, 마지막 sentinel `"untitled"`). `build_canonical_document` 가 lift 후 helper 호출. parser_version 상수 `pulldown-cmark-0.x` → `md-frontmatter-v2` bump — 기존 doc 은 `doc_id` 가 갱신되므로 다음 ingest 가 자동 재처리 (idempotent upsert, design §9 cascade). spec: `tasks/p9/p9-fb-07-md-title-fallback.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-08)** — TUI search async worker + generation counter. 기존 200ms debounce 후 `kebab_app::search_with_config` 동기 호출이 vector/hybrid 모드 50-200ms 동안 UI freeze 시키던 문제 해소. `SearchState` 에 `generation: u64` + `worker_thread: Option<JoinHandle>` + `worker_rx: Option<Receiver<SearchWorkerMessage>>` 신규. `fire_search` 가 spawn 만 하고 즉시 return — worker 가 별 thread 에서 검색 후 `(generation, Result)` 를 channel 로 post. run loop 가 매 tick `poll_worker` 로 try_recv, generation 일치 시 hits 적용 / 불일치 시 silently 폐기 (사용자가 더 빠르게 타이핑하면 stale 결과 자동 drop). debounce_due 가 `searching && last_query == 현 input` 케이스 추가 skip — in-flight worker 의 결과 기다리는 동안 동일 query 재 spawn 안 함. spec: `tasks/p9/p9-fb-08-search-debounce.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-05)** — `workspace.root` path policy 명확화. `kebab_config::expand_path_with_base(raw, data_dir, base_dir) -> PathBuf` 신규 — 기존 `expand_path` (tilde + env 만) 위에 relative path resolution 추가, 절대/`~`/`${VAR}` 입력은 base_dir 무시. `Config.source_dir: Option<PathBuf>` 필드 (`#[serde(skip)]`) 신규 — `from_file` / `load` 가 `path.parent()` 로 stamp. `Config::resolve_workspace_root()` helper 가 `expand_path_with_base(&workspace.root, "", source_dir.unwrap_or(cwd))` 호출. kebab-app + kebab-source-fs 의 모든 `workspace.root` 사용 사이트가 `cfg.resolve_workspace_root()` 로 통일 — kebab-source-fs 의 fork 된 `expand_tilde` 헬퍼는 제거 (kebab-app 의 `storage.data_dir` 한 곳만 남음, P+ 통일 caveat). `kebab init` 가 생성하는 `config.toml` 위에 path policy 안내 헤더 코멘트 자동 prepend (절대/tilde/env/상대 + 상대 base = config dir). spec: `tasks/p9/p9-fb-05-config-path-policy.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-19)** — In-process LRU search cache + `corpus_revision` 카운터. SQLite V004 migration 으로 `kv (key TEXT PK, value TEXT)` 테이블 + `corpus_revision = '0'` seed. `SqliteStore::corpus_revision()` / `bump_corpus_revision()` 메서드 (`UPDATE ... CAST AS INTEGER + 1` 으로 atomic). `kebab-app::ingest_with_config_cancellable` 가 `new + updated > 0` 시 bump — no-op reingest 는 cache 보존. `App.search_cache: Option<Mutex<LruCache<SearchCacheKey, Vec<SearchHit>>>>` (capacity from `config.search.cache_capacity`, default 256, 0 = 비활성). `SearchCacheKey` = `query_norm` (NFKC + trim + lowercase) + `mode` + `k` + `snippet_chars` + `embedding_version` + `chunker_version` + `corpus_revision` snapshot. `App::search` 가 lookup → miss 시 `search_uncached` → put. `search_uncached_with_config` facade 추가, CLI `kebab search --no-cache` 로 bypass (디버깅용). frozen design §9 versioning 표에 `corpus_revision` row 추가. spec: `tasks/p9/p9-fb-19-search-cache.md`.
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-19)** — In-process LRU search cache + `corpus_revision` 카운터. SQLite V004 migration 으로 `kv (key TEXT PK, value TEXT)` 테이블 + `corpus_revision = '0'` seed. `SqliteStore::corpus_revision()` / `bump_corpus_revision()` 메서드 (`UPDATE ... CAST AS INTEGER + 1` 으로 atomic). `kebab-app::ingest_with_config_cancellable` 가 `new + updated > 0` 시 bump — no-op reingest 는 cache 보존. `App.search_cache: Option<Mutex<LruCache<SearchCacheKey, Vec<SearchHit>>>>` (capacity from `config.search.cache_capacity`, default 256, 0 = 비활성). `SearchCacheKey` = `query_norm` (NFKC + trim + lowercase) + `mode` + `k` + `snippet_chars` + `embedding_version` + `chunker_version` + `corpus_revision` snapshot. `App::search` 가 lookup → miss 시 `search_uncached` → put. `search_uncached_with_config` facade 추가, CLI `kebab search --no-cache` 로 bypass (디버깅용). frozen design §9 versioning 표에 `corpus_revision` row 추가. spec: `tasks/p9/p9-fb-19-search-cache.md`. **→ 후속(spine #214 + cleanup): LRU search cache 자체가 제거됨 — `App.search_cache`/`SearchCacheKey`/`App::search_uncached`/`search_uncached_with_config` facade/`search --no-cache` 플래그 모두 삭제. 이 항목은 이력이며 캐시·플래그는 더 이상 존재하지 않음 (`search_cache: false` wire capability 만 정직하게 유지). `corpus_revision` 카운터는 incremental ingest 용도로 잔존.**
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-17)** — Multi-turn chat session 영속화 (storage 만 — UI 는 p9-fb-18). SQLite V005 migration (spec 의 V004 가 p9-fb-19 의 kv 와 충돌해서 V005 로 시프트, HOTFIXES) 으로 `chat_sessions` (session_id PK + created_at + updated_at + title + config_snapshot_json) + `chat_turns` (turn_id PK + session_id FK ON DELETE CASCADE + turn_index + question + answer + citations_json + created_at, UNIQUE(session_id, turn_index)) + `idx_chat_turns_session` 추가. `kebab_core::ChatSessionRepo` trait 6 메서드 (create_session / get_session / list_sessions / delete_session / append_turn / list_turns) + `kebab_core::{ChatSessionRow, ChatTurnRow}` 신규 export. `kebab-store-sqlite::SqliteStore` impl (별 `chat_sessions.rs` 모듈) — append_turn 이 insert + parent updated_at bump 을 같은 conn 에서 처리. frozen design §5 storage 에 §5.7a chat_sessions/turns 절 신설. spec: `tasks/p9/p9-fb-17-chat-session-storage.md`. unblocks p9-fb-18 (CLI session/repl).
|
||||
- **2026-05-03 P9 도그푸딩 후속 (p9-fb-18)** — CLI `kebab ask --session <id>` (multi-turn). p9-fb-17 의 ChatSessionRepo 위에 `kebab-app::App::ask_with_session(session_id, query, opts) -> Answer` 메서드. 첫 호출 시 자동으로 `chat_sessions` row 생성 (title = 첫 question NFC trim 40 chars), 이후 호출은 `list_turns` 로 prior history 받아 `RagPipeline::ask_with_history` 호출 + 새 turn append. `App` 의 helper: `first_question_title(question)` (NFC + trim + 40 char cap, fallback `"untitled"`) + `blake3_truncate(input)` (32-hex `turn_id` 생성). facade `kebab_app::ask_with_session_with_config` + CLI `--session <id>` flag 추가. `--repl` 은 spec 명시 사항이지만 stdin loop fixture 부담 으로 후속 task 로 deferral (out of scope per HANDOFF). spec: `tasks/p9/p9-fb-18-cli-ask-session-repl.md`.
|
||||
- **2026-05-04 P9 post-도그푸딩 (p9-fb-23)** — Incremental ingest. 사용자 도그푸딩 피드백: 변하지 않은 문서는 다시 ingest 하지 않기. blake3 checksum + parser_version + chunker_version + embedding_version 4개 input 이 모두 일치할 때 parse/chunk/embed/vector upsert 모두 회피. SQLite V006 마이그레이션 — `documents` 에 `last_chunker_version` + `last_embedding_version` 컬럼 추가. 신규 `IngestItemKind::Unchanged` variant + `IngestReport.unchanged` + `AggregateCounts.unchanged` (wire schema additive). `IngestOpts { progress, cancel, force_reingest }` struct 도입 — `AskOpts` 패턴. `--force-reingest` CLI flag 로 skip 우회. 비용 dominator (fastembed) 가 변경된 / 새 doc 에만 발생. spec: `tasks/p9/p9-fb-23-incremental-ingest.md`. HOTFIXES `2026-05-04 — p9-fb-23` 항목이 version cascade 명시 동작의 source of truth.
|
||||
|
||||
@@ -250,13 +250,6 @@ impl App {
|
||||
/// — long-lived callers (kb-eval, future TUI) get amortized cost
|
||||
/// across calls.
|
||||
pub fn search(&self, query: SearchQuery) -> Result<Vec<SearchHit>> {
|
||||
self.search_uncached(query)
|
||||
}
|
||||
|
||||
/// p9-fb-19: bypass the LRU cache and run the search directly.
|
||||
/// Used by `--no-cache` CLI invocations and by `search` itself
|
||||
/// on cache miss. Identical behavior to the pre-fb-19 `search`.
|
||||
pub fn search_uncached(&self, query: SearchQuery) -> Result<Vec<SearchHit>> {
|
||||
let mut hits = match query.mode {
|
||||
SearchMode::Lexical => {
|
||||
let lex = LexicalRetriever::with_settings(
|
||||
@@ -363,8 +356,8 @@ impl App {
|
||||
..query.clone()
|
||||
};
|
||||
|
||||
// p9-fb-37: when --trace is requested, bypass the LRU cache and
|
||||
// run through `HybridRetriever::search_with_trace`, which
|
||||
// p9-fb-37: when --trace is requested, run through
|
||||
// `HybridRetriever::search_with_trace`, which
|
||||
// dispatches by mode internally. Vector / hybrid modes require
|
||||
// embeddings (same as `--mode hybrid`); lexical mode skips
|
||||
// embedder construction via `NoopRetriever` so lexical-only
|
||||
@@ -398,16 +391,16 @@ impl App {
|
||||
let hybrid = HybridRetriever::new(&self.config.search, lex, vec_retr);
|
||||
let (mut traced_hits, trace) = hybrid.search_with_trace(&fetch_query)?;
|
||||
|
||||
// Stamp staleness — same as search_uncached.
|
||||
// Stamp staleness — same as `search`.
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
crate::staleness::mark_stale_in_place(
|
||||
&mut traced_hits,
|
||||
now,
|
||||
self.config.search.stale_threshold_days,
|
||||
);
|
||||
// p10-1A-2: backfill code_lang — same as search_uncached.
|
||||
// p10-1A-2: backfill code_lang — same as `search`.
|
||||
backfill_code_lang(&mut traced_hits);
|
||||
// p10-1A-2 Task 8b: backfill repo — same as search_uncached.
|
||||
// p10-1A-2 Task 8b: backfill repo — same as `search`.
|
||||
self.backfill_repo(&mut traced_hits);
|
||||
|
||||
// Apply offset + k_effective truncation (mirrors non-trace path).
|
||||
@@ -437,8 +430,8 @@ impl App {
|
||||
}
|
||||
|
||||
// backfill_code_lang + backfill_repo are applied inside `search`
|
||||
// via `search_uncached` — no explicit call needed here. Trace
|
||||
// branch above calls them directly because it bypasses `search`.
|
||||
// — no explicit call needed here. The trace branch above calls
|
||||
// them directly because it bypasses `search`.
|
||||
let mut all_hits = self.search(fetch_query)?;
|
||||
|
||||
// Skip offset.
|
||||
|
||||
@@ -244,17 +244,6 @@ pub fn search_with_config(
|
||||
App::open_with_config(config)?.search(query)
|
||||
}
|
||||
|
||||
/// p9-fb-19: bypass the LRU search cache for one call. Same shape as
|
||||
/// [`search_with_config`] but routes through [`App::search_uncached`]
|
||||
/// — used by `kebab search --no-cache`.
|
||||
#[doc(hidden)]
|
||||
pub fn search_uncached_with_config(
|
||||
config: kebab_config::Config,
|
||||
query: SearchQuery,
|
||||
) -> anyhow::Result<Vec<SearchHit>> {
|
||||
App::open_with_config(config)?.search_uncached(query)
|
||||
}
|
||||
|
||||
/// p9-fb-34: budget-aware search free function. Mirrors
|
||||
/// [`search_with_config`] but threads `SearchOpts` (max_tokens,
|
||||
/// snippet_chars, cursor) and returns the [`SearchResponse`]
|
||||
|
||||
@@ -81,26 +81,6 @@ fn cache_key_normalization_treats_case_and_whitespace_as_equivalent() {
|
||||
assert_eq!(plain_ids, upper_ids);
|
||||
}
|
||||
|
||||
/// p9-fb-19 — `--no-cache` (`search_uncached_with_config`) bypasses
|
||||
/// the cache. Result correctness is identical to `search_with_config`.
|
||||
#[test]
|
||||
fn search_uncached_returns_same_hits_as_cached() {
|
||||
let env = TestEnv::lexical_only();
|
||||
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();
|
||||
let uncached = kebab_app::search_uncached_with_config(
|
||||
env.config.clone(),
|
||||
common::lexical_query("ownership"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(cached.len(), uncached.len());
|
||||
for (a, b) in cached.iter().zip(uncached.iter()) {
|
||||
assert_eq!(a.chunk_id, b.chunk_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// p9-fb-19 — first ingest with commits bumps `corpus_revision` from
|
||||
/// 0 to ≥1. Verified by reading the persisted kv via a fresh
|
||||
/// SqliteStore handle (the field on `App` is `pub(crate)`).
|
||||
|
||||
@@ -114,19 +114,6 @@ enum Cmd {
|
||||
#[arg(long, value_enum, default_value_t = ModeFlag::Hybrid)]
|
||||
mode: ModeFlag,
|
||||
|
||||
#[arg(long)]
|
||||
explain: bool,
|
||||
|
||||
/// p9-fb-19: bypass the in-process LRU search cache for
|
||||
/// this invocation. Forces a fresh retriever run even when
|
||||
/// the same query was just served from cache. Useful when
|
||||
/// debugging retriever behavior — and a no-op for the CLI
|
||||
/// (each invocation is a new process anyway, so the cache
|
||||
/// starts empty), but the flag stays for parity with the
|
||||
/// future TUI cache-aware search and for explicit intent.
|
||||
#[arg(long)]
|
||||
no_cache: bool,
|
||||
|
||||
/// p9-fb-34: cap result wire JSON size at approximately N tokens
|
||||
/// (chars/4 estimate). When set, smaller snippets and fewer hits
|
||||
/// may be returned; check `truncated` in the JSON wire.
|
||||
@@ -825,8 +812,6 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
query,
|
||||
k,
|
||||
mode,
|
||||
explain: _,
|
||||
no_cache: _,
|
||||
max_tokens,
|
||||
snippet_chars,
|
||||
cursor,
|
||||
|
||||
@@ -66,7 +66,6 @@ snippet_chars = 220
|
||||
[rag]
|
||||
prompt_template_version = "rag-v4"
|
||||
score_gate = 0.30
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
"#,
|
||||
workspace = workspace.display(),
|
||||
|
||||
@@ -67,7 +67,6 @@ snippet_chars = 220
|
||||
[rag]
|
||||
prompt_template_version = "rag-v4"
|
||||
score_gate = 0.30
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
"#,
|
||||
workspace = workspace.display(),
|
||||
|
||||
@@ -92,7 +92,6 @@ stale_threshold_days = {stale_threshold_days}
|
||||
[rag]
|
||||
prompt_template_version = "rag-v4"
|
||||
score_gate = 0.30
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
"#,
|
||||
workspace = workspace.display(),
|
||||
|
||||
@@ -335,7 +335,6 @@ pub struct RagCfg {
|
||||
pub prompt_template_version: String,
|
||||
#[serde(serialize_with = "ser_f32_clean")]
|
||||
pub score_gate: f32,
|
||||
pub explain_default: bool,
|
||||
pub max_context_tokens: usize,
|
||||
/// p9-fb-41: hard ceiling on the number of multi-hop iterations
|
||||
/// (decompose iter + decide iters). When the LLM keeps returning
|
||||
@@ -1057,7 +1056,6 @@ impl Config {
|
||||
rag: RagCfg {
|
||||
prompt_template_version: "rag-v4".to_string(),
|
||||
score_gate: 0.30,
|
||||
explain_default: false,
|
||||
max_context_tokens: 8000,
|
||||
multi_hop_max_depth: default_multi_hop_max_depth(),
|
||||
multi_hop_max_sub_queries_per_iter: default_multi_hop_max_sub_queries_per_iter(),
|
||||
@@ -1662,7 +1660,7 @@ impl Config {
|
||||
}
|
||||
|
||||
// rag — prompt template version is a release-level toggle;
|
||||
// the per-call tuning knobs (score_gate, explain_default,
|
||||
// the per-call tuning knobs (score_gate,
|
||||
// max_context_tokens, multi_hop_*, nli_threshold) stay
|
||||
// config-only.
|
||||
"KEBAB_RAG_PROMPT_TEMPLATE_VERSION" => {
|
||||
@@ -1871,7 +1869,6 @@ snippet_chars = 220
|
||||
[rag]
|
||||
prompt_template_version = "rag-v3"
|
||||
score_gate = 0.3
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
|
||||
[image.ocr]
|
||||
@@ -2423,7 +2420,6 @@ stale_threshold_days = 30
|
||||
[rag]
|
||||
prompt_template_version = "rag-v2"
|
||||
score_gate = 0.30
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
"#;
|
||||
let c: Config = toml::from_str(toml_text).expect("pre-P6 TOML must still parse");
|
||||
|
||||
@@ -86,7 +86,6 @@ stale_threshold_days = 30
|
||||
[rag]
|
||||
prompt_template_version = "rag-v3"
|
||||
score_gate = 0.30000001192092896
|
||||
explain_default = false
|
||||
max_context_tokens = 8000
|
||||
multi_hop_max_depth = 3
|
||||
multi_hop_max_sub_queries_per_iter = 5
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! `chunks.source_spans_json` column and need identical mapping logic
|
||||
//! so cross-mode citation strings round-trip byte-identically (a
|
||||
//! requirement for the hybrid retriever's tie-break on chunk_id and
|
||||
//! for the `search --explain` output documented in design §0 Q3 and
|
||||
//! §1.6). Living here means a future PDF / image / audio extractor can
|
||||
//! for citation provenance in `search --trace` output, design §0 Q3).
|
||||
//! Living here means a future PDF / image / audio extractor can
|
||||
//! enrich the mapping in one place rather than two.
|
||||
|
||||
use kebab_core::{Citation, SourceSpan, WorkspacePath};
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
//! `m`'s output (chunks not appearing in `m` contribute 0).
|
||||
//!
|
||||
//! Each `SearchHit.retrieval` is rebuilt with the per-mode scores /
|
||||
//! ranks the fusion observed, so `kb search --explain` (§1.6) can
|
||||
//! show users exactly which retriever contributed what to the final
|
||||
//! ordering.
|
||||
//! ranks the fusion observed, so `kebab search --trace` can show
|
||||
//! exactly which retriever contributed what to the final ordering.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
@@ -62,8 +61,8 @@ pub enum FusionPolicy {
|
||||
/// and `embedding_model` — lexical search has FTS5 highlighting that's
|
||||
/// more user-relevant than the vector retriever's truncated text.
|
||||
/// Vector-only chunks fall through to the vector hit's data verbatim.
|
||||
/// This matches `kb search --explain` (§1.6) expectations for snippet
|
||||
/// provenance.
|
||||
/// This keeps snippet provenance stable in the fused result's
|
||||
/// `search --trace` output.
|
||||
pub struct HybridRetriever {
|
||||
lexical: Arc<dyn Retriever>,
|
||||
vector: Arc<dyn Retriever>,
|
||||
|
||||
@@ -585,7 +585,7 @@ fn build_hit(
|
||||
chunker_version: ChunkerVersion(raw.chunker_version),
|
||||
indexed_at,
|
||||
// Placeholder — overwritten by `kebab_app::staleness::mark_stale_in_place`
|
||||
// (called from `App::search` / `App::search_uncached`) and the equivalent
|
||||
// (called from `App::search`) and the equivalent
|
||||
// in `RagPipeline::ask` against the configured threshold.
|
||||
stale: false,
|
||||
score_kind: ScoreKind::Bm25,
|
||||
|
||||
@@ -348,7 +348,7 @@ fn build_hit(
|
||||
chunker_version: ChunkerVersion(meta.chunker_version.clone()),
|
||||
indexed_at,
|
||||
// Placeholder — overwritten by `kebab_app::staleness::mark_stale_in_place`
|
||||
// (called from `App::search` / `App::search_uncached`) and the equivalent
|
||||
// (called from `App::search`) and the equivalent
|
||||
// in `RagPipeline::ask` against the configured threshold.
|
||||
stale: false,
|
||||
score_kind: ScoreKind::Cosine,
|
||||
|
||||
@@ -424,15 +424,7 @@ $KB search 'tokenizer' --mode lexical --json | jq '.hits | length' # ≥ 1 if co
|
||||
- `next_cursor` opaque token.
|
||||
- `corpus_revision` mismatch → `stale_cursor` error.
|
||||
|
||||
### §2.6 Search cache (p9-fb-19)
|
||||
|
||||
```bash
|
||||
"$RELEASE_BIN" search "query" --json # first call
|
||||
"$RELEASE_BIN" search "query" --json # cached (in-process LRU, no-op in CLI)
|
||||
"$RELEASE_BIN" search "query" --no-cache --json # force fresh
|
||||
```
|
||||
|
||||
### §2.7 Bulk search
|
||||
### §2.6 Bulk search
|
||||
|
||||
stdin ndjson — 줄당 하나의 query object (`{"query":"<text>"}` 필수, 나머지 optional):
|
||||
```bash
|
||||
|
||||
@@ -42,7 +42,7 @@ classDiagram
|
||||
ingest_with_config_progress
|
||||
ingest_with_config_cancellable
|
||||
list_docs_with_config / inspect_*_with_config
|
||||
search_with_config / search_uncached_with_config
|
||||
search_with_config
|
||||
ask_with_config / ask_with_session_with_config
|
||||
doctor_with_config_path
|
||||
}
|
||||
@@ -117,7 +117,7 @@ flowchart LR
|
||||
- `ingest_with_config(cfg, scope, summary_only)` — 가장 단순.
|
||||
- `ingest_with_config_progress(cfg, scope, progress: Option<Sender<IngestEvent>>)` — TTY 진행 표시 / `--json` line-delimited 용.
|
||||
- `ingest_with_config_cancellable(cfg, scope, progress, cancel: Option<Arc<AtomicBool>>)` — 위 + cooperative cancel. asset loop iter 시작에서 poll → true 면 break + `Aborted{partial_counts}` + `Ok(IngestReport)` 반환 (Err 아님). 부분 commit 보존.
|
||||
- `search_with_config / search_uncached_with_config` — 후자는 LRU cache bypass (debug).
|
||||
- `search_with_config` — one-shot `App` 생성 후 `App::search` 위임.
|
||||
- `ask_with_session_with_config(cfg, session_id, query, opts)` — multi-turn (p9-fb-18).
|
||||
- `doctor_with_config_path(Option<&Path>)` — config 경로 explicit.
|
||||
|
||||
@@ -125,7 +125,7 @@ flowchart LR
|
||||
- `App::open_with_config(cfg) -> Result<Self>` — SQLite open + migration. embedder/vector/llm 은 lazy `OnceLock` 으로 첫 호출에서 build.
|
||||
- `App::embedder() -> Option<...>` — `provider == "none"` 또는 `dimensions == 0` 이면 `None` (lexical-only fallback).
|
||||
- `App::ask_with_session(session_id, query, opts)` — repo + RAG ask + 새 turn append 한 묶음 (p9-fb-18).
|
||||
- `App::search(query)` — LRU cache lookup → miss 시 `search_uncached` → put. cache key = `(query_norm, mode, k, snippet_chars, embedding_version, chunker_version, corpus_revision)`.
|
||||
- `App::search(query)` — 설정된 retriever stack 실행 후 top-k hits 반환. staleness stamp + `code_lang` / `repo` backfill 을 hit 에 적용.
|
||||
|
||||
**`IngestEvent`** (`kebab-app::ingest_progress`):
|
||||
- `Started { total }` / `AssetStarted { workspace_path }` / `AssetCompleted { counts }` / `Completed { counts }` / `Aborted { partial_counts }`. terminal frame 후 sender drop.
|
||||
|
||||
@@ -130,7 +130,7 @@ flowchart LR
|
||||
|
||||
## 관련 spec / HOTFIXES
|
||||
|
||||
- frozen 설계 §3.7 (SearchHit), §6.4 (`search.hybrid_fusion`/`rrf_k`/`default_k`/`snippet_chars`), §0 Q3 (citation), §1.6 (`--explain`), §7.2 (`Retriever` trait): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
|
||||
- frozen 설계 §3.7 (SearchHit), §6.4 (`search.hybrid_fusion`/`rrf_k`/`default_k`/`snippet_chars`), §0 Q3 (citation), §7.2 (`Retriever` trait): [`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`](../../superpowers/specs/2026-04-27-kebab-final-form-design.md)
|
||||
- task spec:
|
||||
- lexical: [`tasks/p2/p2-2-search-lexical.md`](../../../tasks/p2/p2-2-search-lexical.md)
|
||||
- vector + hybrid: [`tasks/p3/p3-4-hybrid-fusion.md`](../../../tasks/p3/p3-4-hybrid-fusion.md)
|
||||
|
||||
Reference in New Issue
Block a user