From 923b959610182a20745811e5183233063d960c04 Mon Sep 17 00:00:00 2001 From: altair823 Date: Thu, 28 May 2026 11:13:45 +0000 Subject: [PATCH] refactor(app): retire short_query_hint helper, keep wire field as None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V009 unicode61 + 형태소 tokenizer 환경에서 2-char 한국어 query 가 hit 가능해졌으므로 V007 시기의 "3자 이상 권장" hint 가 obsolete. SearchResponse.hint field 는 wire schema 보존 위해 struct 에 유지 + 항상 None. - kebab-app/src/app.rs: short_query_hint 함수 + doc-comment 삭제. 2 호출 site 가 hint = None 으로 정리. - kebab-app/src/lib.rs: re-export 에서 short_query_hint 제거. - kebab-tui/{app.rs,search.rs,run.rs}: short_query_hint field + 4 호출 cascade 제거. - kebab-cli/tests/wire_search_response.rs: search_plain_emits_short_query_hint_to_stderr test 삭제. search_json_emits_hint_field_for_short_query → search_json_hint_absent_for_short_query_v009 으로 교체 (hint 항상 None 검증). - kebab-search/src/lexical.rs::build_match_string: V007 의 trigram multi-token OR-combine 분기는 V009 환경에서 redundant 하나 보존 (future 확장성) — doc-comment 1 줄 추가. Wire schema shape 변경 없음 (search_response.schema.json:33 의 hint field 보존, struct 에 None 으로 항상 셋팅). Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §7.2, §7.3, §11.3 Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S5) Co-Authored-By: Claude Sonnet 4.6 --- crates/kebab-app/src/app.rs | 27 +------------ crates/kebab-app/src/lib.rs | 2 +- .../kebab-cli/tests/wire_search_response.rs | 39 ++++--------------- crates/kebab-search/src/lexical.rs | 3 ++ crates/kebab-tui/src/app.rs | 7 ---- crates/kebab-tui/src/run.rs | 14 ------- crates/kebab-tui/src/search.rs | 9 ----- 7 files changed, 13 insertions(+), 88 deletions(-) diff --git a/crates/kebab-app/src/app.rs b/crates/kebab-app/src/app.rs index e64b51f..87e51a2 100644 --- a/crates/kebab-app/src/app.rs +++ b/crates/kebab-app/src/app.rs @@ -89,29 +89,6 @@ pub struct SearchResponse { pub hint: Option, } -/// v0.17.0 A5 Step 4b: decide whether to attach a "3자 이상 키워드 권장" -/// hint to a `SearchResponse`. Fires only when the result set is empty -/// *and* the trimmed query is shorter than the trigram tokenizer can -/// resolve. Raw FTS5 mode (`'...'`) opts out — the user explicitly -/// invoked FTS5 syntax. Identical condition powers the CLI stderr line -/// and (separately) the TUI status bar. -pub fn short_query_hint(query_text: &str, hits_empty: bool) -> Option { - if !hits_empty { - return None; - } - let trimmed = query_text.trim(); - let bytes = trimmed.as_bytes(); - // Raw single-quote mode: user opted into FTS5 syntax, no advisory. - if bytes.len() >= 2 && bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'' { - return None; - } - if trimmed.chars().count() < 3 { - Some("3자 이상 키워드 권장 (trigram tokenizer 제약)".to_string()) - } else { - None - } -} - /// Facade state — see module docs for lifetime rules. /// /// The struct is public so long-lived callers (kb-eval, the future P9 @@ -557,7 +534,7 @@ impl App { // Trace path skips the budget loop. Caller will inspect // `hits.len()` and `trace.timing` rather than paginate. - let hint = short_query_hint(&query.text, hits.is_empty()); + let hint: Option = None; return Ok(SearchResponse { hits, next_cursor: None, @@ -641,7 +618,7 @@ impl App { None }; - let hint = short_query_hint(&query.text, hits.is_empty()); + let hint: Option = None; Ok(SearchResponse { hits, next_cursor, diff --git a/crates/kebab-app/src/lib.rs b/crates/kebab-app/src/lib.rs index 1f289fc..b5c30bd 100644 --- a/crates/kebab-app/src/lib.rs +++ b/crates/kebab-app/src/lib.rs @@ -72,7 +72,7 @@ pub mod reset; pub mod schema; mod staleness; -pub use app::{App, SearchResponse, short_query_hint}; +pub use app::{App, SearchResponse}; #[doc(hidden)] pub use bulk::{BULK_QUERIES_MAX, bulk_search_with_config}; pub use error_wire::{ERROR_V1_ID, ErrorV1, StructuredError, classify}; diff --git a/crates/kebab-cli/tests/wire_search_response.rs b/crates/kebab-cli/tests/wire_search_response.rs index 26b24ec..75d256f 100644 --- a/crates/kebab-cli/tests/wire_search_response.rs +++ b/crates/kebab-cli/tests/wire_search_response.rs @@ -212,32 +212,10 @@ fn search_plain_emits_truncated_hint_to_stderr() { } #[test] -fn search_plain_emits_short_query_hint_to_stderr() { - // v0.17.0 A5 Step 6: 2-char query under trigram tokenizer emits - // empty hits + stderr `[hint]` advisory. Empty workspace is enough - // — hits are always empty so the hint condition depends only on - // query length (<3 chars trimmed) + non-raw mode + hits.is_empty. - let dir = tempfile::tempdir().unwrap(); - let (cfg, workspace, _data) = common::write_config(dir.path(), 30); - common::ingest(&cfg, &workspace); - - let (_stdout, stderr) = common::run_search_with_args(&cfg, &["--mode", "lexical", "ab"]); - assert!( - stderr.contains("[hint]"), - "stderr must carry short-query hint: {stderr:?}" - ); - assert!( - stderr.contains("3자 이상"), - "hint message must mention '3자 이상' (Korean advisory): {stderr:?}" - ); -} - -#[test] -fn search_json_emits_hint_field_for_short_query() { - // v0.17.0 A5 Step 6: --json mode carries the same advisory on the - // `search_response.v1.hint` additive field. Empty hits + 2-char - // query + non-raw mode trips the helper. Verifies the MCP-visible - // surface (agents read the field instead of parsing stderr). +fn search_json_hint_absent_for_short_query_v009() { + // V009 unicode61 + 형태소 tokenizer 환경에서는 2-char 한국어 query 도 + // hit 가능하므로 short_query_hint helper 가 제거됨. hint 는 항상 + // None — wire schema field 는 유지되나 JSON 에서 omit 됨. let dir = tempfile::tempdir().unwrap(); let (cfg, workspace, _data) = common::write_config(dir.path(), 30); common::ingest(&cfg, &workspace); @@ -250,12 +228,9 @@ fn search_json_emits_hint_field_for_short_query() { v["hits"].as_array().unwrap().is_empty(), "empty hits expected for short query in empty KB: {v}" ); - assert_eq!( - v["hint"] - .as_str() - .expect("hint field set on short empty result"), - "3자 이상 키워드 권장 (trigram tokenizer 제약)", - "hint must carry the standard advisory: {v}" + assert!( + v.get("hint").is_none(), + "hint must be absent (always None post-V009): {v}" ); } diff --git a/crates/kebab-search/src/lexical.rs b/crates/kebab-search/src/lexical.rs index 8b30b3c..6176ea4 100644 --- a/crates/kebab-search/src/lexical.rs +++ b/crates/kebab-search/src/lexical.rs @@ -202,6 +202,9 @@ impl Retriever for LexicalRetriever { /// - Finally wrap the combined expression in `text : ()` so the /// match is scoped to the body column. FTS5's column-filter syntax /// accepts an arbitrary OR/AND sub-expression inside the parens. +/// +/// V009 unicode61 + 형태소 tokenizer 환경에서는 multi-token Korean +/// query 의 OR-combine 분기는 redundant 하나 보존 (future 확장성). fn build_match_string(text: &str) -> Option { let trimmed = text.trim(); if trimmed.is_empty() { diff --git a/crates/kebab-tui/src/app.rs b/crates/kebab-tui/src/app.rs index 50ca759..d8722d2 100644 --- a/crates/kebab-tui/src/app.rs +++ b/crates/kebab-tui/src/app.rs @@ -153,12 +153,6 @@ pub struct SearchState { /// `Ctrl-L`); the previous draft kept one for "symmetry" but /// it was dead code. pub worker_rx: Option>, - /// v0.17.0 A5 Step 5: advisory text shown when the last completed - /// search returned no hits and the (trimmed) query is shorter than - /// the FTS5 trigram tokenizer's 3-char minimum. `None` whenever - /// the input changes (so a stale hint never overlaps a fresh - /// typing session) or the next search returns ≥1 hit. - pub short_query_hint: Option, } /// p9-fb-08: payload posted by the search worker on completion. @@ -185,7 +179,6 @@ impl Default for SearchState { preview: None, generation: 0, worker_rx: None, - short_query_hint: None, } } } diff --git a/crates/kebab-tui/src/run.rs b/crates/kebab-tui/src/run.rs index 2ec42a8..e42bceb 100644 --- a/crates/kebab-tui/src/run.rs +++ b/crates/kebab-tui/src/run.rs @@ -382,20 +382,6 @@ fn dynamic_status(app: &App) -> String { if app.search.as_ref().is_some_and(|s| s.searching) { return "searching…".to_string(); } - // v0.17.0 A5 Step 5: short-query advisory has higher priority than - // the idle slot but lower than active operations (streaming / - // searching / ingest progress) — the user should always see what - // is happening *now* before reading guidance about the last - // empty result. Slot only fires while focused on Search. - if app.focus == Pane::Search { - if let Some(hint) = app - .search - .as_ref() - .and_then(|s| s.short_query_hint.as_deref()) - { - return hint.to_string(); - } - } if let Some(state) = app.ingest_state.as_ref() { return crate::ingest_progress::status_line(state); } diff --git a/crates/kebab-tui/src/search.rs b/crates/kebab-tui/src/search.rs index 85618c7..7cf5d17 100644 --- a/crates/kebab-tui/src/search.rs +++ b/crates/kebab-tui/src/search.rs @@ -440,7 +440,6 @@ pub fn handle_key_search(state: &mut App, key: KeyEvent) -> KeyOutcome { /// with a fresh typing session. fn mark_input_changed(s: &mut crate::app::SearchState) { s.input_dirty_at = Some(time::OffsetDateTime::now_utc()); - s.short_query_hint = None; } fn cycle_mode(m: SearchMode) -> SearchMode { @@ -612,11 +611,6 @@ pub(crate) fn fire_search(state: &mut App) -> anyhow::Result<()> { s.generation = s.generation.wrapping_add(1); s.searching = true; s.input_dirty_at = None; - // v0.17.0 A5 Step 5: hint belongs to the *prior* result set — - // a fresh worker spawn invalidates it so the status bar - // doesn't keep showing the old advisory while the new - // query is in flight. - s.short_query_hint = None; let q_text = s.input.as_str().to_string(); s.last_query = Some((q_text.clone(), s.mode)); (q_text, s.mode, s.generation) @@ -699,8 +693,6 @@ pub fn poll_worker(state: &mut App) { // the user submitted for *this* result set. If // input has drifted since spawn, the gen-check // already returned early. - let q_text = s.last_query.as_ref().map_or("", |(t, _)| t.as_str()); - s.short_query_hint = kebab_app::short_query_hint(q_text, hits.is_empty()); s.hits = hits; s.selected_hit = 0; s.preview = None; @@ -708,7 +700,6 @@ pub fn poll_worker(state: &mut App) { Err(e) => { s.hits.clear(); s.selected_hit = 0; - s.short_query_hint = None; state.error_overlay = Some(crate::error_popup::ErrorOverlay::from_anyhow(&e)); } }