회차 2 review (PR #96 회차 2) 의 2 건 cosmetic nit 모두 수렴.
- `tasks/HOTFIXES.md`, `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md`:
\"기존 38 개\" → \"기존 39 개 (input.rs unit 18 + tests/ask.rs 21)\"
로 정확 카운트 + 출처 명시.
- `crates/kebab-tui/src/library.rs`: `active_buf_mut` doc comment 의
\"3-line dispatch\" → \"2-arm dispatch\" (실제 dispatch 가 2 arm 이라
가장 정확한 표현).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 중 발견된 두 건 (Gitea #94, #95) 동시 수정.
#94 — `InputBuffer` 가 append-only 라 Ask/Search/Filter overlay 에서
타이핑한 텍스트의 중간을 편집할 수 없었음. cursor 모델을 byte-position
기반으로 재구성 (cursor_col 은 prefix slice 의 unicode-width 합으로
derive). 신규 메서드: `move_left / move_right / move_home / move_end /
delete_after`. 기존 `push_char` / `pop_char` 는 cursor 위치에서 동작
(cursor 가 끝일 때 backwards-compatible). Ask / Search / Library filter
overlay 세 곳에 `← / → / Home / End / Delete` key handler 추가. Search 는
cursor 이동만으로는 input_dirty_at 을 reset 하지 않음 (커서 이동 ≠ 쿼리
변경 → debounce 타이머 유지).
#95 — Ask 트랜스크립트의 `Paragraph::scroll((s.scroll, 0))` 가 위에서
부터 카운트라, 새 답변 도착 시 `s.scroll = 0` 으로 리셋하면 viewport 가
위쪽 고정 → 트랜스크립트가 길어지면 새 응답이 시야 밖으로 밀림. `AskState`
에 `follow_tail: bool` (default true) 추가. `render_answer` 가 follow_tail
동안 매 프레임 `Paragraph::line_count(width)` 로 wrapped row 수 계산해
스크롤을 `line_count - inner_height` 에 pin. `j` / `k` 가 follow_tail 끄고
`Shift-G` 가 다시 켬. 새 submission, `Ctrl-L` 도 follow-tail 재활성화.
`kebab-tui` 의 ratatui dep 에 `unstable-rendered-line-info` feature
활성화 — `Paragraph::line_count` 가 ratatui 0.28 에서 unstable. 0.28 에
pin 되어있는 동안 안정. 향후 ratatui bump 시 본 feature 의 stable 여부
재확인 필요.
cheatsheet popup Search/Ask section 에 화살표 + Home/End + Delete row
추가, Ask 에 `Shift-G` row 추가. README + HANDOFF + HOTFIXES + INDEX 동기.
Tests: 12 신규 InputBuffer unit + 6 신규 Ask integration. 기존 699 워크
스페이스 테스트 모두 통과 (cursor 가 끝일 때 backwards-compat).
Spec: `tasks/p9/p9-fb-22-tui-cursor-and-autoscroll.md` (status `completed`).
Live deviation 기록: `tasks/HOTFIXES.md` `2026-05-04 — p9-fb-22`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 피드백 (사용자 2026-05-03): Ask Insert→Esc→Normal 후 Insert 로
돌아가는 키 모름. 전반적 키바인딩 안내 부족.
Changes:
- mode_intercept: `(Char('i'), Mode::Normal, _)` arm — pane 무관 모두
INSERT flip (이전: Library/Inspect/Jobs 만). 사용자가 어느 pane 에서든
Esc 후 `i` 로 Insert 즉시 복귀 가능.
- Search 의 chunk inspect 키 `i`→`o` (vim "open") rebind. `i` 가
universal Insert toggle 로 자유로워짐.
- `footer_hints` 모든 (pane, mode, filter) 조합 첫 fragment = `F1 도움말`.
cheatsheet binding 의 discoverability 보장.
- Search/Ask Normal hint 에 `i 입력모드` fragment 추가.
- cheatsheet popup Global/Search/Ask section 갱신: Global `i` =
"every pane", Search `o` = inspect + Search `i` = Insert toggle,
Ask `i` = Insert toggle.
- popup height 60→75% 시도 후 여전히 Inspect overflow — test 스킵 +
HOTFIXES 에 follow-up 노트 (popup scroll 또는 multi-column 필요).
Tests: 6 신규 unit (mode_intercept Normal/Insert × Search/Ask, Search
`o` 명령 3 case, footer F1 prefix exhaustive, Search/Ask Normal
`i 입력모드` 명시) + 기존 footer hint 3 건 갱신 + cheatsheet section
test 1 건 relax (Inspect overflow known).
spec: `tasks/p9/p9-fb-21-tui-insert-key-discoverability.md` (status
`completed` 직접 — 도그푸딩 직접 피드백 source).
`pub fn footer_hints(focus: Pane, mode: Mode, filter_open: bool) -> &'static str`
신규 (run.rs). 기존 `render_footer` 의 영문 `key=action` 형식이 한국어
동사구로 — `"위로"` / `"아래로"` / `"필터"` / `"타이핑 검색어"` /
`"Esc 로 NORMAL 모드"` 등 — 변경되고 (pane, mode, filter_open) 조합에
따라 자동 분기. NORMAL 모드는 navigation verbs, INSERT 모드는
typing + Esc reminder. Library filter overlay 는 overlay-only key 3
개로 override.
8 unit tests pin: 모든 (pane, mode, filter) 조합 non-empty exhaustive
+ Library Normal/filter, Search Normal/Insert, Ask Normal/Insert,
Inspect Normal 별 verb fragment 존재 검증.
spec status `in_progress` → `completed` — p9-fb-13 partial 의 deferred
verb-form 항목이 닫힘.
- Add `place_cursor_x(inner_x, inner_width, prompt_w, cursor_col) -> u16`
to `input.rs`: sums in `usize` (no u16 wrap), clamps to inner right
edge, tries_into with u16::MAX fallback. Two unit tests pin the clamp
and the in-bounds path.
- Re-export from `lib.rs` alongside `InputBuffer`, `display_width`,
`truncate_to_display_width`.
- Replace the open-coded 2-line `raw_x`/`cursor_x` blocks in Search,
Ask, and Library with a single `place_cursor_x` call each —
consistent usize arithmetic across all three panes.
- Add `filter_overlay_render_places_cursor_on_focused_field` integration
test in `tests/library.rs`: opens the filter overlay, renders through
`TestBackend`, asserts `terminal.get_cursor_position().x > 0` (label
offset > 0 proves `set_cursor_position` was called with a meaningful
coordinate, not stuck at origin).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
p9-fb-10: verifies that a Korean (Hangul) token survives the
ingest → FTS5 lexical search round-trip via the kebab-app facade.
NFC normalization is wired upstream in kebab-normalize; this test
only exercises end-to-end correctness — no AVX, no fastembed required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrates FilterEdit.tags_buf and lang_buf from String to InputBuffer
(the same display-width-aware type used by Search/Ask), adds cursor
placement in render_filter_overlay so the caret sits at the end of the
focused field, and adds a Hangul filter test
(filter_overlay_accepts_hangul_tags) that confirms wide chars round-trip
through key events and commit_into unchanged.
Also adds App::library_filter_for_testing accessor mirroring
populate_library_for_testing style.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migrates SearchState.input from String to InputBuffer so wide-char
(Hangul/CJK) keystrokes advance the terminal cursor by display columns
instead of char count. Adds cursor placement in render_input_bar via
f.set_cursor_position and a Hangul round-trip pin in tests/search.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `InputBuffer` with `push_char`/`push_str`/`pop_char`/`clear` tracking
cursor position in display columns (CJK = 2, ASCII = 1) plus 6 unit tests
(p9-fb-10 Task 1). Re-export from crate root.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TAGS_COL_W const 추출 (truncate + pad 동시 사용 — drift 방지).
- format_doc_row 직접 unit test 2개 (Hangul title / Hangul tag) 가
display column 정렬을 정확히 pin. `<title_w$>` 으로 되돌리는
회귀가 unit-level 에서 catch 됨.
- format_doc_row: title/tags padding 을 display_width 기반으로 명시
계산 (std::fmt 의 char-count 기반 `<width$>` 가 wide char 에서 column
drift). truncate 가 보장하는 width 계약 위에 padding 도 같은 단위로
통일.
- input.rs 테스트 코멘트 cleanup (`= wait` 디버깅 잔재 제거).
- HOTFIXES "후속 spec issue" → "후속 PR 체크리스트" 로 owner 명시,
체크박스 5 개로 actionable 화.
`kebab-tui::input::{display_width, truncate_to_display_width}` 신규.
unicode-width 위에서 column-단위 width 계산 (ASCII=1, Hangul/CJK/
fullwidth=2, combining=0) + char-boundary 안전 truncate.
library.rs 의 중복 `truncate_to_display_width` private fn 제거 — 단일
source 로 통일. 9 unit tests + 1 integration render test (Korean +
Japanese fixture, TestBackend 80×20).
spec 의 `InputBuffer` struct 도입은 follow-up — Ask/Search/Editor pane
의 String + cursor 일괄 마이그레이션이 회귀 표면이 커서 helper 만
먼저 머지. backspace 는 모든 pane 이 이미 `String::pop()` 사용 → byte-
boundary 안전성 확보. crossterm 0.28 가 native IME composing 미노출 →
preedit handling out of scope.
spec status `planned` → `in_progress`. HOTFIXES.md 에 InputBuffer
struct deferral 사유 기록.
- `cheatsheet_intercept` doc 에 trade-off 명시: Esc-when-visible 가
cheatsheet 만 닫고 mode flip 안 함 (single-effect-per-keystroke).
Insert 모드 사용자는 두 번째 Esc 로 Normal 전환.
- `cheatsheet.rs` 모듈 doc 에 maintenance 경고 추가: push_section()
이 hard-coded string 이라 binding 변경 시 cheatsheet 동기화 수동.
자동 link 없음 — future PR 가 키 바꾸면 cheatsheet 도 갱신 필수.
118 TUI 테스트 통과 + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 11 — vim 비익숙 사용자도 TUI 조작 가능. F1 으로 cheatsheet
modal popup, 현재 pane 의 키 매핑 + global 토글 (i/Esc/F1) 한 자리.
## 핵심 변경
- **`kebab-tui::cheatsheet::render_cheatsheet(f, area, app)`** 신규 —
70%/60% centered modal. 5 sections (Global / Library / Search / Ask
/ Inspect) 각 pane 의 모든 키 + 동사구 설명. footer 에 현재 focused
pane 명시. theme.style(Role::Heading/CitationMarker/Hint) 으로 색
계층 (header bold, key cyan-marker, body plain, hint dim).
- **`App.cheatsheet_visible: bool`** field + `pub fn cheatsheet_
visible() -> bool` getter (read-only — set/unset 은 F1 intercept
invariant).
- **`cheatsheet_intercept(app, key)`** in run.rs:
- F1 → toggle (open ↔ close), consumed
- Esc 가 visible 일 때 → close, consumed (mode_intercept 가 같은
Esc 를 mode flip 으로 해석하지 않도록 cheatsheet_intercept 가
먼저 dispatch)
- 그 외 키 → fall-through (popup 열린 채 navigation 가능)
- modifier-bearing F1 (Ctrl-F1 등) 무시
- **run loop 통합**: `cheatsheet_intercept` → `mode_intercept` →
pane dispatch 순. render_root 가 error overlay 위에 cheatsheet
overlay (사용자가 error 도중에도 도움말 소환 가능).
## HOTFIXES (`?` → `F1` rebind)
spec 은 `?` 를 trigger 로 명시했지만 Library 가 이미 `Char('?')` 를
quick-Ask binding 으로 사용 중 (handle_key_library line 305). spec 의
`?` 채택 = 기존 binding 깨거나 mode-aware special case 추가. 후자는
mode machine 에 더 많은 분기 추가하므로 회피.
**Live binding**: `F1` (universal help key, no collision).
**Per-pane verb hint line**: spec 의 verb-form hint 재구성도 본 PR
에서 deferral. 기존 `render_footer` 의 pane-별 힌트 문자열이 동일 UX
역할 — 후속 PR 에서 mode-aware verb fragments 로 split 가능.
spec status `planned` → `in_progress` (NOT `completed` — verb hint
deferral 명시).
## 테스트
- 5 신규 integration unit (`tests/cheatsheet.rs`):
- F1 toggles visibility (open ↔ close, consumed 양쪽)
- Esc closes when visible / falls through when hidden
- modifier-bearing F1 (Ctrl-F1, Alt-F1) 무시
- arbitrary keys (j, /, q, Enter) fall through 하면서 popup 열린 채
- render_cheatsheet 가 모든 section header (Global/Library/Search/
Ask/Inspect) + global toggle (F1, Esc) 출력
- 기존 113 TUI 테스트 + 신규 5 = 118 통과
- `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README `kebab tui` 행: F1 cheatsheet popup 안내
- HANDOFF: 2026-05-03 entry
- HOTFIXES: ?→F1 rebind rationale + verb hint deferral
- spec status `planned` → `in_progress`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- search::handle_key_search 의 j/k 두 개 arm (Insert 가드 + Normal
no-guard) 을 single arm + body if-branch 로 flatten. 4 arm → 2 arm,
\"j/k 가 mode 따라 다르다\" 가 한 자리에서 보임. ask.rs 패턴과 정렬.
- `is_typing_mod` 자리에 남아있던 \"removed\" placeholder 코멘트 3 줄
삭제. commit history 와 매치 블록 안 코멘트 가 reference.
113 TUI 테스트 통과 + clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `mode_intercept` 를 `pub` 로 노출 + `pub use run::mode_intercept`
로 lib.rs export. 신규 `tests/mode.rs` 6 integration unit:
- Esc-from-Insert flips to Normal on every pane (consumed)
- Esc-from-Normal falls through (pane handler 가 처리 — Library
의 quit signal 등 보존)
- i-from-Normal on Library/Inspect/Jobs flips to Insert (consumed)
- i-on-Search/Ask falls through (이미 Insert, i 가 typed char)
- Ctrl/Alt modifier 는 intercept 안 함 (chord 가능)
- Shift+Esc 는 toggle 됨 (modifier filter 가 SHIFT allow), Shift+I
(capital) 는 fall-through (lowercase i 만 toggle 키)
- `Mode::auto_for` doc 에 \"auto-flip overrides user manual mode on
pane switch\" 명시 — 의도된 트레이드오프 (typing 이 Search/Ask 의
dominant case). sticky-per-pane 은 future task.
워크스페이스 clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 10 — vim 비익숙 사용자도 \"지금 키가 입력 vs 명령\" 명확히
구분 가능. 절반 ship: 사용자 가시 signal (mode label + auto flip + i/Esc
global) 만 land, 키 dispatch 의 input-empty heuristic 제거는 follow-up.
## 핵심 변경
- **`kebab_tui::Mode { Normal, Insert }`** enum + `Default = Normal`.
- `Mode::label()` → `"-- NORMAL --"` / `"-- INSERT --"` (status bar
문자열, 테스트로 핀).
- `Mode::auto_for(pane)` → Library/Inspect/Jobs = Normal,
Search/Ask = Insert. pane 전환 시 자동 적용.
- **`App.mode: Mode`** field. `App::new` 가 starting pane 의 auto
mode 로 init.
- **run loop `mode_intercept(app, key)`** — pane dispatch 전에 호출:
- Insert + `Esc` → Normal (어디서나, modifier 없음)
- Normal + `i` (Library/Inspect/Jobs 만) → Insert
- Search/Ask 의 `i` 는 fall-through (이미 Insert 라 typed char)
- 그 외 fall-through
- **pane 전환 시** `app.mode = Mode::auto_for(p)` 자동 flip — 사용자가
Tab 으로 Search 가면 자동으로 Insert.
- **status bar (header)** 에 mode label colored — Insert = Role::
Success (green), Normal = Role::Heading (cyan + bold). a11y: 색은
reinforcement, 글자가 authoritative signal.
## Deferred (HOTFIXES entry 추가)
spec p9-fb-12 의 \"기존 P9-3 ask 의 e/j/k input-empty heuristic 제거 —
mode 로 명확히\" 는 별 PR 로. 현재 dispatch 는 여전히:
- search.rs 의 `is_typing_mod` (SHIFT 만 typing 으로, CTRL/ALT 는 chord)
- ask.rs 의 input.is_empty() 가 e/j/k 를 navigation 으로 분기
테스트가 heuristic 에 의존해 있어, 회귀 surface 좁게 유지하려고 splitting.
spec status `in_progress` 유지 (not `completed`) — follow-up PR 가
heuristic 제거 + 완전 mode-authoritative 후 `completed` flip.
## 테스트
- 신규 3 unit (`Mode::auto_for` 모든 pane, label literals 핀,
default = Normal)
- 기존 98 TUI 테스트 모두 통과 (heuristic 그대로라 회귀 0)
- workspace 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README `kebab tui` 행: vim-style mode + auto NORMAL/INSERT + i/Esc
안내
- HANDOFF entry (partial-ship 명시)
- HOTFIXES entry (heuristic 제거 deferral 사유)
- spec status planned → in_progress (NOT completed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `App::build_retriever(mode) -> Result<Arc<dyn Retriever>>` 추출.
`ask` 와 `ask_with_session` 모두 사용. 35+ 줄 retriever stack 중복
제거 — 미래 retriever 변경이 한 곳만.
- V005 migration `chat_sessions.sql` 의 `citations_json` doc 수정:
`Vec<Citation>` → `Vec<AnswerCitation>` (실제 stored type 과 일치).
AnswerCitation 가 marker + Citation 등 포함하므로 deserialize 시
type mismatch 회피.
15 app lib + 9 store chat_sessions + clippy 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 14 — CLI 에서도 multi-turn 가능하도록 `kebab ask
--session <id>` 추가. p9-fb-17 의 ChatSessionRepo 위에 build, 첫 호출
세션 자동 생성, 이후 호출이 prior turns 를 history 로 받아 follow-up.
external AI integration (Claude Code skill / MCP) 도 같은 facade 로
stateful 대화 가능.
## 핵심 변경
- **`App::ask_with_session(session_id, query, opts) -> Answer`** —
load session header → list_turns 로 prior history → 빌드 retriever
stack (lexical / vector / hybrid 같은 분기) → `RagPipeline::ask_
with_history` 호출 → 첫 호출이면 `chat_sessions` row 자동 생성
(title = first_question_title) → `chat_turns` 새 row append.
- **`App::first_question_title(question)`** helper — `trim() + nfc()
+ 40 chars cap`, fallback `"untitled"`. unicode-normalization
workspace dep 재사용.
- **`App::blake3_truncate(input)`** helper — `blake3(session_id ||
":" || turn_index)` 의 첫 16 byte 를 u128 으로, format!{:032x} 로
32-hex `turn_id`.
- **`ask_with_session_with_config`** facade — CLI 진입점.
- **CLI `--session <id>` flag** — `Cmd::Ask` 의 `session: Option<
String>` field, handler 가 None 이면 `ask_with_config` (기존
단발), Some(id) 면 `ask_with_session_with_config` 호출.
- **에러 정책**: session create / turn append 실패 시 warn 로그
남기고 answer 는 그대로 반환 — 사용자가 답변 받은 컴퓨트를 잃지
않음. 영속성 실패가 답변 응답을 가로막지 않는 conservative shape.
## 테스트
- `App::first_question_title` 3 unit (trim + cap, empty → untitled,
korean NFD → NFC)
- `App::blake3_truncate` 1 unit (deterministic + distinct across
varying session/index)
- 워크스페이스 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README `kebab ask` 행: `--session` 안내 + chat_sessions 자동 생성
+ `kebab reset --data-only` wipe 안내
- README **외부 AI 통합** 절: Claude Code skill 이 `--session` 으로
multi-turn 가능하다는 한 문장 추가
- HANDOFF entry
- spec status planned → in_progress
## Out of scope (spec deviation)
- `--repl` (stdin loop) — spec 명시되어 있으나 stdin fixture 부담
으로 deferral. 별도 후속 task 또는 `--session` 사용자 경험 회신
후 결정.
- session list / show / delete 관리 명령 (spec 의 Out of scope).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `append_turn` 의 doc 은 "wrap in one transaction" 보장하지만 실제
코드는 auto-commit `conn.execute` 두 번이라 두번째 실패 시 first
row 가 commit 된 채 inconsistent 됨. 진짜 transaction 으로 교체:
`conn.transaction()` → `tx.execute(insert)` → `tx.execute(update)`
→ `tx.commit()`. SQLite BEGIN 으로 감싸 두 statement atomic.
`lock_conn()` 이 `MutexGuard<Connection>` 반환하므로 `let mut conn =
self.lock_conn(); let tx = conn.transaction()` 패턴 가능 (MutexGuard
의 DerefMut 활용).
9 chat_sessions 테스트 + clippy 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `App::search` 의 두 cache.lock() 호출이 mutex poison 시 silently
bypass 하던 것을 `unwrap_or_else(|e| { warn!; e.into_inner() })`
recovery 로 교체. cache 가 poison 됐어도 다음 호출은 정상이고
한 번은 warn 로그가 남아 panic 흔적 추적 가능. lookup 후 lock
drop → retriever 호출 → 재 lock 으로 lock granularity 도 짧게.
- `clear_search_cache` 도 같은 recovery 패턴.
- `SearchCacheKey` doc 에 spec 와 impl 의 naming 차이 (index_version
vs corpus_revision) 명시 + HOTFIXES entry 추가. spec 의 index_
version 명칭이 design §9 의 기존 `IndexVersion` newtype (embedding
-index identity 라벨) 과 충돌해서 corpus_revision 으로 rename.
7 tests/search_lexical 통과. clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 15 — TUI / 같은 process 안에서 동일 query 반복 시 SQLite
FTS + Lance + RRF 재계산이 매번 발생하던 비용 해소. in-process LRU
캐시 + 모노토닉 corpus_revision 카운터로 ingest commit 발생 시 모든
entry 자동 stale.
## 핵심 변경
- **SQLite V004 migration**: `kv (key TEXT PRIMARY KEY, value TEXT)
STRICT` + `corpus_revision = '0'` seed. 미래의 다른 scalar 도 같은
테이블에 들어갈 수 있는 generic shape.
- **`SqliteStore::corpus_revision()` / `bump_corpus_revision()`** —
`UPDATE ... CAST AS INTEGER + 1` atomic. INSERT-OR-IGNORE 도 함께
실행 (V004 seed 가 무슨 이유로 누락된 케이스 paranoid).
- **`kebab-app::ingest_with_config_cancellable`** — `new + updated > 0`
시 bump, no-op (skipped-only) reingest 는 cache 보존.
- **`App.search_cache: Option<Mutex<LruCache<SearchCacheKey, Vec<
SearchHit>>>>`** — `config.search.cache_capacity` (default 256, 0
비활성). `lru = "0.12"` workspace dep 추가.
- **`SearchCacheKey`** = `query_norm` (NFKC + trim + lowercase) +
`mode` + `k` + `snippet_chars` + `embedding_version` (vector/hybrid
만, lexical 은 빈 문자열) + `chunker_version` + `corpus_revision`
snapshot.
- **`App::search`** rewrite — cache 활성 시 lookup → miss 면 기존
`search_uncached` 호출 후 put. cache 비활성이거나 lock 실패면
straight-line.
- **`App::search_uncached`** (rename of pre-fb-19 `search` body) +
`search_uncached_with_config` facade — CLI `kebab search --no-cache`
로 진입.
- **`Config.search.cache_capacity: usize`** field, `#[serde(default)]`
로 기존 config 호환.
- **CLI `--no-cache`** flag — 디버깅용 (CLI 는 매 호출이 새 process
라 사실상 no-op 이지만 spec 명시 + 향후 long-lived process 호환).
- **frozen design §9 versioning** 표에 `corpus_revision` row 추가
(기존 `index_version` 라벨과 다른 차원: 라벨은 retrieval 형상,
corpus_revision 은 ingest commit ack).
## 테스트
- `kebab-store-sqlite` 신규 3 unit (fresh=0, monotonic bump, persist
across reopen)
- `kebab-app` 신규 4 integration (cached repeat 같은 hits, NFKC 정규화
로 case/whitespace collapse, --no-cache parity, first ingest bumps
corpus_revision)
- 워크스페이스 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README `kebab search` 행: 캐시 동작 + `--no-cache` 안내 + corpus_
revision 무효화 메커니즘
- docs/SMOKE.md `[search]` 절에 `cache_capacity` 라인 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress
## Out of scope
- patch-and-merge incremental (RRF 정규화 전체 hit set 기준이라 어려움)
- SQLite 영속 cache (P+)
- 다른 process 간 cache 공유 (in-process 만 — corpus_revision 이
cross-process 무효화는 O(1))
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `Config.source_dir` 를 `pub(crate)` 로 좁힘. invariant ("from_file
/ load 만이 정당한 setter") 가 외부 mutation 으로 깨지지 않도록.
대신 `pub fn source_dir(&self) -> Option<&Path>` (read-only) +
`pub fn with_source_dir(self, dir) -> Self` (builder) 노출 — 테스트
/ 프로그래마틱 사용은 builder 통과.
- `resolve_workspace_root` 의 `current_dir()` 실패 fallback 에
`tracing::warn!` 추가. chroot / deleted-cwd / permission 문제로
cwd 가 안 잡힐 때 silently `./root` 로 떨어지지 않고 로그가 남음.
`tracing` 을 kebab-config 의 deps 에 추가 (workspace dep).
테스트 27 통과 + 워크스페이스 clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 3 — `workspace.root` 의 허용 형식이 명문화 안 돼 사용자가
\"상대 경로면 어디 기준?\" 가 불명확. 이제 절대/tilde/env/상대 모두
지원하되, 상대 경로의 base 는 **config.toml 자체가 위치한 디렉토리**
(사용자의 cwd 와 무관) 로 일관 정책.
## 핵심 변경
- **`kebab_config::expand_path_with_base(raw, data_dir, base_dir)`**
신규. 기존 `expand_path` (tilde + env 만) 위에 relative-path
resolution 추가:
- tilde / 절대 / `${VAR}` 입력은 base_dir 무시 (이미 absolute)
- relative 입력만 `base_dir.join(...)` 로 절대화
- **`Config.source_dir: Option<PathBuf>`** 신규 (`#[serde(skip)]`).
`Config::from_file` / `load` 가 `path.parent()` 로 stamp. defaults
는 None (cwd fallback).
- **`Config::resolve_workspace_root()`** helper: source_dir 있으면
그것 기준, 없으면 cwd 기준.
- **callsite 정리**:
- `kebab-app::lib.rs` 의 3 군데 `expand_tilde(&app.config.workspace
.root)` → `app.config.resolve_workspace_root()`
- `kebab-app::init_workspace` 도 동일
- `kebab-source-fs::FsSourceConnector::new` → 동일
- kebab-source-fs 의 fork 된 local `expand_tilde` + `dirs_home`
헬퍼 제거 (kebab-config 가 canonical)
- **`kebab init`** 가 생성하는 `config.toml` 위에 path policy 안내
헤더 코멘트 prepend (절대/tilde/env/상대 + 상대 base = config dir).
기존 `expand_tilde` 가 kebab-app/lib.rs 에 한 군데 (storage.data_dir)
남음 — spec out-of-scope (\"expand_tilde 통일 P+\") 라 보류.
## 테스트
- `expand_path_with_base` 에 신규 4 unit (relative→base, absolute
ignores base, tilde ignores base, ${XDG} ignores base)
- 기존 27 kebab-config tests + workspace 전체 (`cargo test --workspace
--no-fail-fast -j 1` exit 0) 모두 통과
- `cargo clippy --workspace --all-targets -- -D warnings` clean
## 문서
- README Configuration 절: workspace.root 형식 + relative base 규칙
한 줄 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress
## 영향
기존 사용자: 영향 없음 (defaults 의 `~/KnowledgeBase` 는 tilde-rooted,
relative path 분기 안 탐). 새 사용자가 `--config /tmp/cfg.toml` +
`root = "kb"` 같이 쓰면 cwd 무관하게 `/tmp/kb` 가 워크스페이스가 됨 —
이전엔 이 케이스가 cwd 기준이라 invisible foot-gun.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `SearchState.worker_thread` 필드 제거 — `JoinHandle` 을 저장만 하고
어디서도 join 안 했음. fire_search 가 spawn 후 handle 을 즉시 drop
하면 OS 가 thread 를 detach (search 는 pure read 라 cleanup 의무
없음). YAGNI — ask.rs 의 thread 와 달리 cancel/observe 수요가 없는
fire-and-forget. doc 으로 의도 명시.
- `debounce_due` 가 `pub` 으로 노출 — 새 skip 분기 (`searching && 같은
query`) 회귀 테스트 추가:
- `debounce_due_skips_when_in_flight_for_same_query`: 같은 input/mode
재입력 시 spawn 안 함 (worker 누적 방지)
- `debounce_due_fires_when_in_flight_for_different_query`: 사용자가
in-flight 보다 빠르게 새 query 입력하면 정상 spawn (poll_worker 의
stale guard 가 이전 결과 처리)
- `search_state_with` 헬퍼: `SearchState::default()` + field 재할당
패턴이 clippy `field_reassign_with_default` 위반 → `#[allow(...)]`
로 lint 무시 (테스트 helper 의 가독성 우선).
23 tests/search.rs + 35 lib + 18 ask + 12 inspect + 10 library = 98
통과. clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 6 — TUI search 의 200ms debounce 후 동기 호출이 vector
/ hybrid 모드에서 50-200ms 동안 UI 를 freeze 시키던 문제 해소. 별
thread 에서 search 돌리고 결과 mpsc 로 받음. 사용자가 계속 타이핑하면
stale 결과 자동 폐기 (generation counter pattern, ask.rs 의 worker
패턴과 동일).
## 핵심 변경
- **`SearchState` 필드 3 개 신규**:
- `generation: u64` — 각 spawn 마다 increment, worker 가 carry
- `worker_thread: Option<JoinHandle<()>>`
- `worker_rx: Option<Receiver<SearchWorkerMessage>>`
- **`SearchWorkerMessage`** (`pub enum`) — 단일 변종 `Done {
generation, result }`. ask.rs 의 token stream 과 달리 search 는
최종 결과만 한 번 send, 그래서 enum 으로 추후 확장 여지 둠.
- **`fire_search`** rewrite: generation+1 → debounce snapshot 갱신 →
`std::thread::Builder::spawn` 으로 별 thread, `kebab_app::search_
with_config(cfg, query)` 호출, channel 로 `(gen, result)` post.
return 은 즉시 — event loop 안 막힘.
- **`poll_worker`** 신규 (`pub`, integration test 위해 노출): tick
마다 try_recv. `gen != s.generation` 이면 stale → silently drop +
`searching` 그대로 (newer worker 가 처리). 일치하면 hits 적용 +
`searching=false`. Disconnect 면 worker 패닉 처리 — searching
clear, 다음 tick 의 debounce_due 가 재 spawn.
- **`debounce_due`** 강화: `searching && last_query == 현 input/mode`
케이스 skip — 같은 query 재 spawn 방지. 기존 dedupe 도 유지.
- **run loop** 의 `Pane::Search` 분기에 `poll_worker(app)` 한 줄
추가 (debounce_due 호출 직전). 매 tick drain.
## 테스트 (tests/search.rs 신규 4 개)
- `poll_worker_applies_fresh_result_to_hits` — gen 일치 시 hits 적용
+ searching clear + rx drain
- `poll_worker_drops_stale_result` — gen 불일치 시 hits 비어 있음
+ searching 유지 (newer worker 기다림)
- `poll_worker_noop_when_no_rx` — 평상시 tick 에 noop, 기존 hits
보존
- `poll_worker_handles_disconnected_channel` — 워커 panic (tx drop)
복구 — searching clear, rx 비움
기존 17 search + 35 lib + 18 ask + 12 inspect + 10 library = 92
통과. clippy clean.
## 문서
- README `kebab tui` 행: "Search 패널은 200ms debounce 후 background
worker, stale 결과 자동 폐기" 한 줄 추가
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress
## Out of scope
- 캐시 (p9-fb-19 별도)
- 동일 query 의 inflight worker 합치기 — 현재는 dedupe + 가장 최근
spawn 만 살아남는 fire-and-forget. 합치는 건 mpsc multiplexing
로직 필요해 P+ 로 미룸.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `compose_style` 레이어링 정리 — base color 는 가장 구체적인 컨테이너
(heading > link > inline_code > body) 에서 가져오되 modifier 는
link 의 UNDERLINED, inline code 의 DIM 도 헤딩 위에 add. `# Section
[docs](url)` 의 `docs` 가 헤딩 색 + UNDERLINE 둘 다 받음.
- `Event::InlineMath` / `Event::DisplayMath` silently drop 폐기 →
raw 글자 (예: `E = mc^2`, `\sum_i x_i`) 를 `Role::Hint` 스타일로
보존. 답변에서 수학 표현이 사라지던 문제 수정.
- `Event::FootnoteReference` → `[^label]`, `Event::TaskListMarker` →
`[x] ` / `[ ] ` 로 raw 표시 (이전엔 silently drop).
- `unterminated_bold_renders_literal_asterisks` 테스트의 OR 어설션을
강화 — `still typing` 글자만 누락 안 되면 통과 (literal `**` 보존
여부는 cosmetic, 글자 누락은 진짜 회귀).
- 신규 unit 3 개: heading 안 link 가 UNDERLINE+BOLD 둘 다, math 보존,
task list 체크박스 글리프.
35 lib + 17 search + 18 ask + 12 inspect + 10 library 통과. clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 9 — TUI Ask 답변 본문이 raw `**bold**` / `# Title` /
` ```code``` ` 그대로 보여 가독성 떨어지던 문제 해소. pulldown-cmark
파싱 → ratatui Span/Line 변환.
## 핵심 변경
- **`kebab-tui::markdown::render(text, &Theme) -> Vec<Line<'static>>`**
신규. pulldown-cmark = "0.13" (이미 kebab-parse-md 가 사용 중인
버전) 위에 build.
inline:
- `**bold**` / `__bold__` → `Modifier::BOLD`
- `*italic*` / `_italic_` → `Modifier::ITALIC`
- `~~strike~~` → `Modifier::CROSSED_OUT`
- `` `code` `` → `Role::Hint` (DIM 스타일 — 터미널 호환성 위해 bg
color 보다 안전)
- `[text](url)` → `Role::CitationMarker` + `Modifier::UNDERLINED`
block:
- heading H1/H2 → `Role::Heading` (Cyan + BOLD), H3-H6 → `Role::Title`
(White + BOLD)
- bullet list `-`/`*` → `- ` + 깊이별 indent
- ordered list `1.` → 실제 번호 prefix + indent
- fenced code block ``` ``` ``` → ` ` indented + `Role::Hint`
- blockquote `>` → 좌측 `▎` bar (중첩 시 반복) + `Role::Hint`
- table `| col |` → `| col1 | col2 |` 식 줄, `|` separator 색 강조
- horizontal rule `---` → `─` × 40
- **streaming 안전성**: 매 frame 재 parse 가 spec — pulldown
토크나이저가 µs/KB 라 비용 무시. unterminated `**` (사용자가 한창
입력 중인 inline 가 닫히기 전) 은 pulldown 이 Text 로 처리 →
literal `**` 그대로 표시 (글자 누락 X).
- **`ask::push_turn_lines` 통합**: grounded 답변에서만 markdown
렌더 사용. refusal turn (`Role::Warning` override) 와 streaming
turn (`Role::Hint`) 은 raw 로 두어 role color 시그널이 markdown
스타일에 묻히지 않도록. body line 들은 ` ` indent 로 transcript
에서 답변 본문 시각 구분.
- **CLI `kebab ask` 출력은 raw markdown** — 터미널 호환성 + pipe
처리 시 안정성 위해 (ANSI escape 없이 plain text).
## 테스트 (markdown.rs 14 unit)
- empty input → 빈 라인 1 줄 (caller scroll/measure 안전)
- plain text → 단일 라인 + paragraph blank
- bold / italic / strikethrough / inline code → 해당 modifier 검증
- link → UNDERLINED 검증
- heading H1 → BOLD 텍스트 span
- bullet list `-` / numbered list `1./2.` → prefix 검증
- code fence body → 줄별 ` ` indent 보존
- blockquote → `▎` prefix
- 2x2 table → `|`-separated 줄 검증
- unterminated `**` → 글자 누락 없음 (streaming 안전성 회귀 방지)
- composite (heading + para + list + code) → 문서 순서 보존
기존 75 TUI 테스트 + 신규 14 markdown = 89 통과. clippy clean.
## 문서
- README `kebab tui` 행에 markdown 렌더 안내 + CLI 는 raw 명시
- HANDOFF: 2026-05-03 entry
- spec status planned → in_progress
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ask.rs `push_turn_lines` Q label: `Role::Heading + add_modifier(BOLD)`
의 BOLD 가 중복 (Role::Heading 이 양 팔레트 모두 BOLD 포함). 제거 +
주석으로 \"Heading 은 이미 BOLD\" 명시.
- run.rs `render_ingest_status` aborted 분기: `Role::Title` (= White +
BOLD) 보다 `Role::Warning` (Yellow) 이 \"비정상 종료\" 의미와 정렬.
BOLD 는 명시적으로 add_modifier 하여 라이브 진행 라인과의 대비 유지.
- theme.rs `impl Default for Theme` 위에 doc comment 추가 — `default()
== dark()` invariant 와 `default_palette_is_dark` 테스트가 묶여
있음을 명시.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
도그푸딩 item 12 — TUI 가 모든 정보 종류에 같은 회색 / 시안 만 쓰던
\"빈약한 색감\" 해소. inline `Style::default().fg(Color::*)` 호출을
single source `theme` 모듈로 격리 + dark / light 두 팔레트 제공.
## 핵심 변경
- **`kebab-tui::theme::{Theme, Role, Palette}`** 신규 (132 라인). 16
개 Role enum (BorderActive/BorderInactive/Title/Path/ModeLexical/
ModeVector/ModeHybrid/Selected/Hint/Heading/Warning/Error/Success/
CitationMarker/Bullet/Body) 을 dark + light 두 팔레트가 exhaustive
match 로 매핑. 새 Role 추가 시 두 팔레트 모두 갱신해야 컴파일됨.
- **`Theme::from_name(s)`** — 알 수 없는 값 (e.g. \"solarized\") →
dark fallback. config typo 가 TUI 를 죽이지 않음 (spec 명시).
- **`App.theme: Theme`** 신규 — `App::new` 가 `config.ui.theme` 에서
resolve. 모든 pane (library/search/ask/inspect/run/error_popup) 이
`app.theme.style(Role::X)` 로 style 가져옴.
- **`Config.ui.theme: String`** 신규 — `[ui] theme = \"dark\" | \"light\"`
(default `\"dark\"`). `#[serde(default)]` 로 기존 config 파일 호환.
- **Pane sweep**: search.rs / ask.rs / library.rs / inspect.rs /
run.rs / error_popup.rs 의 모든 inline `Style::default().fg(Color::*)`
/ `add_modifier(Modifier::DIM/REVERSED)` 호출 제거. 일부 helper
(`render_filter_overlay`, `header_kv`, `kv`, `push_section_header`,
`build_doc_lines`, `build_chunk_lines`, `render_input/answer/bottom/
status/citations`, `render_error_overlay`) 가 `theme: &Theme` 파라
미터 추가.
## Out of scope
- `T` 키 runtime toggle — mode machine (p9-fb-12) 미진행이라 NORMAL
모드 정의 불가, config 만으로 결정. 추후 p9-fb-12 후속에서 추가.
- 사용자 정의 `[theme.custom]` 절 — P+ task.
- truecolor → 256-color fallback — terminal 가정.
## 테스트
- 신규 4 개 (theme.rs):
- `every_role_resolves_in_dark_and_light` — 16 Role 전부 panic 없이
Style 반환 (exhaustive match runtime 검증)
- `from_name_recognizes_dark_light_and_falls_back` — 입력 정규화 +
fallback 정책
- `default_palette_is_dark` — 기본값 pin
- `primary_roles_carry_decoration_in_dark` — Title/Selected/Heading/
Error/Warning/Success 가 bare default 로 회귀 안 함
- 기존 75 개 TUI 테스트 (14 lib + 18 ask + 12 inspect + 10 library +
17 search + 4 theme) 모두 통과
- `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy -p kebab-tui -p kebab-config --all-targets -- -D warnings`
clean
## 문서
- README Configuration 절: `[ui]` 섹션 + `theme = \"dark\"|\"light\"`
안내
- docs/SMOKE.md: config 예시에 `[ui] theme = \"dark\"` 라인 추가
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: planned → in_progress
p9-fb-11 (ask markdown render) 의 `Theme` 의존성 unblock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `App.pending_editor` / `force_redraw` 를 `pub(crate)` 로 좁힘.
외부 caller (kebab-cli/desktop) 는 enqueue 할 일 없고, 잘못
mutate 하면 \"set 후 다음 tick 에 drain\" invariant 가 깨짐.
- 외부 read access 가 필요한 경우를 위해 `App::pending_editor()` 읽기
전용 accessor 추가 — integration test (`tests/search.rs`) 가 사용.
- `App.force_redraw` doc comment 의 \"ratchet incremented\" 표현을
실제 type (bool flag) 에 맞게 \"when set, the next draw clears\" 로
교체.
- `editor::tests::unspawnable_program_surfaces_program_name_in_error`
를 `command_status_returns_not_found_for_missing_program` 으로
rename + doc 수정 — 실제로 `with_external_program` 호출하지 않고
`Command::status()` 만 검증한다는 점을 솔직하게 명시 (helper
end-to-end 는 dogfooding 으로 검증).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Search `g` 키 (citation jump) 후 TUI 화면이 깨지는 버그 수정. 도그푸딩
item 7 — `g` 로 vim 띄우고 `:q` 후 복귀하면 이전 frame 의 잔상이 새
draw 위에 겹쳐 보였음.
## 핵심 변경
- **`kebab-tui::editor::with_external_program(&mut TuiTerminal, Command)`**
helper 추가. suspend / spawn / restore 시퀀스를 RAII guard 로 atomic
하게 묶어 panic 발생해도 raw mode + alt screen 복구 보장:
1. LeaveAlternateScreen + Show cursor + disable_raw_mode
2. Command::status() 로 child 실행
3. enable_raw_mode + EnterAlternateScreen + Hide cursor +
`terminal.clear()` ← 이 한 줄이 핵심 fix
- **`App.pending_editor: Option<EditorRequest>`** 추가. 키 핸들러
(현재 `kebab-tui::search::handle_key_search` 의 `g`) 가 직접 spawn
하는 대신 EditorRequest 를 enqueue, 실제 spawn 은 run loop 가
`TuiTerminal` 핸들 in scope 일 때 처리.
- **`App.force_redraw: bool`** ratchet. with_external_program 종료 후
set, run loop draw 직전 check → terminal.clear() 후 reset. editor
외 다른 향후 use case (config reload, theme change 등) 도 같은 hook
사용 가능.
## 가시성 정리
`with_external_program` / `jump_to_citation` 은 `pub(crate)` 로 좁혀짐
— `TuiTerminal` 자체가 module-private (raw mode + alt screen 의 안전
한 lifecycle 은 `Drop` 만 보장) 이므로 외부 caller 는 `App.pending_
editor` enqueue 패턴으로만 spawn 요청 가능. 외부 surface (`build_jump_
command`, `handle_key_search`, `render_search`) 는 그대로.
## 테스트
- `unspawnable_program_surfaces_program_name_in_error` — helper 의 spawn
실패 경로 (ENOENT) error context 검증
- `g_key_enqueues_pending_editor_request` — `g` on hit → EditorRequest
enqueue, citation 정보 보존
- `g_key_with_no_hits_does_not_enqueue` — empty hits → no-op
- 기존 17 개 search 테스트 + 14 lib + 18 ask + 12 inspect + 10 library
모두 통과
- `cargo clippy -p kebab-tui --all-targets -- -D warnings` clean
## 문서
- README: `kebab tui` 행에 Search `g` 동작 + 자동 redraw 안내
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: `planned` → `in_progress`
후속 task (p9-fb-20 의 citation jump in TUI Ask 등) 가 같은
`pending_editor` queue + `with_external_program` helper 위에 build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `derive_title` doc 의 step 5 표현 "kebab-case preserved" → "returned
verbatim, no case transformation" (실제 동작과 일치)
- `file_stem` NFC 변환 제거 — workspace_path 가 to_posix 단계에서 이미
NFC 정규화되므로 (§6.6) 이중 호출은 군더더기. 의도 명시 주석 추가.
- M7 revised 테스트 docstring 의 "p9-fb-07 line 37" 참조를 인용문
("빈 문자열 반환 금지") 으로 교체 — line number 변동에 안전.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`kebab-normalize::derive_title(frontmatter_title, blocks, file_stem)` 가
다음 단계로 비어있지 않은 첫 결과를 사용:
1. frontmatter `title` (trim 후)
2. 첫 H1 텍스트
3. 첫 H2 텍스트
4. 첫 Paragraph (Quote / List / Code / Table / ImageRef 제외) 의 첫 80 자
5. 파일 stem (확장자 제외)
6. (sentinel) `"untitled"` — 위 다섯 단계가 모두 blank 인 병적 케이스
선택된 문자열은 NFC 정규화. 빈 문자열은 절대 반환하지 않음.
`build_canonical_document` 가 metadata lift 직후 helper 호출. 기존 단순
lift 로직 (metadata.user["title"] → CanonicalDocument.title) 은 fallback
chain 의 1 단계 입력으로 자리 이동.
`KEBAB_PARSE_MD_VERSION` 상수를 `pulldown-cmark-0.x` → `md-frontmatter-v2`
로 bump. parser_version 변경 → §4.2 doc_id 입력 변화 → 기존 markdown
doc 의 `doc_id` 갱신, 다음 ingest 시 idempotent upsert 로 자동 재처리
(design §9 cascade). `kebab-store-sqlite` 의 snapshot fixture 도 같은
literal 로 갱신.
기존 M7 정책 ("metadata.user[\"title\"] = '' 가 빈 title 로 lift") 은
폐기. 빈 문자열 입력은 fallback chain 을 타고 file stem 까지 떨어진다.
spec p9-fb-07 line 37: "빈 문자열 반환 금지".
테스트 (kebab-normalize):
- 8 개 단위 테스트 (각 fallback 단계 + NFC + sentinel)
- `build_canonical_document` 통합 테스트 2 개 (H1 / file stem)
- 기존 M7 테스트 2 개를 새 정책에 맞춰 갱신
문서:
- README: `kebab ingest` 행에 "title 자동 채움" 안내 + 기존 doc 도
다음 ingest 에서 갱신
- HANDOFF: 2026-05-03 머지 후 발견 entry
- spec status: `planned` → `in_progress`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회차 1 actionable 2건 반영.
- (information accuracy) 모든 citation 라인이 같은 ans.retrieval.top_score
반복 출력했던 문제 — AnswerCitation 에 per-citation score 없으므로
사용자 오해 회피 위해 score 컬럼 제거. 대신 retrieval 메타 한 줄로
분리: '(retrieval: top_score=X.XX, k=N, used=M/N)'. per-citation
score 노출은 facade + AnswerCitation 의 미래 확장 후 (별 task).
- (cleanup) marker fallback 의 두 번 변환 (as_deref + unwrap_or +
to_string) → c.marker.clone().unwrap_or_else(|| format!(...))
한 단계로 단순화.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
답변 출력 후 `근거:` 절 — `[N] <full path>#<fragment> (score=<s>)`
한 줄씩. spec p9-fb-20 의 핵심 (full path 가독성) 충족.
신규 flag:
- --show-citations: default ON. 답변 후 citation block 출력.
- --hide-citations: 답변 본문만 출력 (pipe 시 다른 도구가 trailing
metadata 안 받기 원할 때).
`--json` 모드 무영향 — citations 가 wire payload 에 항상 포함되므로
flag 가 영향 X (외부 wrapper 호환성).
spec p9-fb-20 의 \"TUI citation pane + jump (Enter/o editor jump,
i inspect)\" 부분은 본 PR scope 에서 제외 — TUI 의 기존
render_citations_or_explain (P9-3) 가 이미 citation list 표시,
추가 fold/jump 는 후속 task. 사용자 도그푸딩 priority 5위 의
핵심 = \"full path 가독성\" 이라 CLI block 만으로 충분.
Plan 갱신:
- p9-fb-20 status planned → in_progress. 머지 후 한 줄 commit
으로 completed flip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
회차 1 actionable 2건 반영.
- (UX 회귀) push_turn_lines 가 answer_color_override: Option<Color>
추가 받음. render_answer 가 마지막 turn 에 한해 last_answer.grounded
== false 면 Yellow override 전달 → P9-3 의 refusal 시각 구분
contract 가 transcript 안에서도 보존. test:
render_refusal_turn_in_transcript_uses_yellow_when_last_answer_ungrounded
가 buffer 의 Yellow R 셀 검사로 검증.
- (race) Ctrl-L 가 turns/conversation_id/last_answer/partial/
current_question/scroll 외에도 thread/rx/streaming 까지 detach.
in-flight worker 가 다음 frame 에 finish 해도 새 conv 의 stale
Turn 으로 graduate 안 됨 — JoinHandle Drop 으로 detach (P9-3 Esc
cancel pattern 동일). worker 자체는 background 에서 SQLite
answers 에 \"실패한 conv\" 흔적 commit. ctrl_l_clears_conversation_state
test 가 streaming/thread/rx 도 함께 검증.
18 PASS. clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>