Commit Graph

284 Commits

Author SHA1 Message Date
570fd86c2a Merge pull request 'feat(kebab-core + kebab-store-sqlite): p9-fb-17 chat session storage (V005)' (#80) from feat/p9-fb-17-chat-storage into main 2026-05-03 05:40:59 +00:00
952ed1615f review(p9-fb-17): 회차 1 nit 반영
- `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>
2026-05-03 05:40:40 +00:00
c97e8e00ef feat(kebab-core + kebab-store-sqlite): p9-fb-17 chat session storage (V005)
도그푸딩 item 13/14 (multi-turn 영속화) — TUI Ask 의 "이전 대화
이어가기" + 향후 CLI `--session foo` (p9-fb-18) backing store. session
header + per-turn 두 테이블, ON DELETE CASCADE 로 reset --data-only 가
한꺼번에 wipe.

## 핵심 변경

- **SQLite V005 migration** `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(session_id, turn_index)`. 모두 `STRICT`.
- **`kebab_core::ChatSessionRepo`** trait (6 method): create_session /
  get_session / list_sessions(limit, ORDER BY updated_at DESC) /
  delete_session / append_turn / list_turns(ORDER BY turn_index ASC)
- **`kebab_core::{ChatSessionRow, ChatTurnRow}`** structs — Serialize
  + Deserialize 둘 다 (CLI / wire 출력 호환)
- **`kebab-store-sqlite::SqliteStore`** impl 신규 모듈 `chat_sessions.rs`.
  `append_turn` 이 insert + parent updated_at bump 같은 connection
  에서 처리.
- **frozen design §5** 에 §5.7a chat_sessions / chat_turns 절 신설
  (full schema + trait 메서드 6 개 명시).

## HOTFIXES (V004 → V005)

spec p9-fb-17 의 `V004__chat_sessions.sql` 가 p9-fb-19 의
`V004__kv.sql` (이미 머지) 와 refinery migration number 충돌. 무중단
정정: `V005__chat_sessions.sql` 로 시프트. schema / 동작 동일, 파일명
만 이동. HOTFIXES entry 추가.

## 테스트

- 9 신규 integration unit (create/get roundtrip, missing→None, PK
  collision error, append+list ordered, dup turn_index error,
  append bumps updated_at, delete CASCADE turns, list_sessions
  ORDER BY updated_at DESC, list_sessions LIMIT)
- workspace 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- frozen design §5.7a 신설
- HANDOFF: 2026-05-03 entry
- HOTFIXES: V004 → V005 rename rationale
- spec status planned → in_progress

## Out of scope

- session 검색 / 필터 UI (p9-fb-18 의 `kebab ask --session list`
  같은 admin command 가 후속)
- 다른 store backend (postgres 등) — trait 만 정의, impl 은 SQLite

unblocks p9-fb-18 (CLI session/repl).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:37:53 +00:00
626f013b97 Merge pull request 'chore(tasks): mark p9-fb-19 completed' (#79) from chore/p9-fb-19-status-completed into main 2026-05-03 05:08:48 +00:00
9601d63c1f chore(tasks): mark p9-fb-19 completed
PR #78 머지 (e9e37fd). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:08:38 +00:00
e9e37fda6f Merge pull request 'feat(kebab-app + kebab-store-sqlite): p9-fb-19 search LRU cache + corpus_revision' (#78) from feat/p9-fb-19-cache into main 2026-05-03 05:08:10 +00:00
d36667589f review(p9-fb-19): 회차 1 nit 반영
- `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>
2026-05-03 05:07:48 +00:00
0e408fb1b5 feat(kebab-app + kebab-store-sqlite): p9-fb-19 search LRU cache + corpus_revision
도그푸딩 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>
2026-05-03 05:01:31 +00:00
07dc2346fe Merge pull request 'chore(tasks): mark p9-fb-05 completed' (#77) from chore/p9-fb-05-status-completed into main 2026-05-03 04:24:33 +00:00
02571367a3 chore(tasks): mark p9-fb-05 completed
PR #76 머지 (18a1c96). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 04:23:53 +00:00
18a1c968f0 Merge pull request 'feat(kebab-config + kebab-app): p9-fb-05 workspace.root path policy' (#76) from feat/p9-fb-05-path into main 2026-05-03 04:23:31 +00:00
702c7c89f7 review(p9-fb-05): 회차 1 nit 반영
- `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>
2026-05-03 04:23:16 +00:00
f6cc612dbe feat(kebab-config + kebab-app): p9-fb-05 workspace.root path policy
도그푸딩 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>
2026-05-03 04:20:06 +00:00
cd4fdd5500 Merge pull request 'chore(tasks): mark p9-fb-08 completed' (#75) from chore/p9-fb-08-status-completed into main 2026-05-03 03:56:15 +00:00
20f5e247f9 chore(tasks): mark p9-fb-08 completed
PR #74 머지 (be0a96e). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:55:58 +00:00
be0a96e10f Merge pull request 'feat(kebab-tui): p9-fb-08 async search worker + generation counter' (#74) from feat/p9-fb-08-search-async into main 2026-05-03 03:55:42 +00:00
a99f81398c review(p9-fb-08): 회차 1 nit 반영
- `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>
2026-05-03 03:55:27 +00:00
fd8597c696 feat(kebab-tui): p9-fb-08 async search worker + generation counter
도그푸딩 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>
2026-05-03 03:50:56 +00:00
29c9668f58 Merge pull request 'chore(tasks): mark p9-fb-11 completed' (#73) from chore/p9-fb-11-status-completed into main 2026-05-03 03:37:47 +00:00
b33d02629b chore(tasks): mark p9-fb-11 completed
PR #72 머지 (cd11897). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:37:24 +00:00
cd11897158 Merge pull request 'feat(kebab-tui): p9-fb-11 ask answer markdown rendering' (#72) from feat/p9-fb-11-markdown into main 2026-05-03 03:37:02 +00:00
ae25ecaad5 review(p9-fb-11): 회차 1 nit 반영
- `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>
2026-05-03 03:36:40 +00:00
c462dbf6a4 feat(kebab-tui): p9-fb-11 ask answer markdown rendering
도그푸딩 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>
2026-05-03 03:33:21 +00:00
2c1ea17222 Merge pull request 'chore(tasks): mark p9-fb-14 completed' (#71) from chore/p9-fb-14-status-completed into main 2026-05-03 03:13:48 +00:00
2d40d60af2 chore(tasks): mark p9-fb-14 completed
PR #70 머지 (e8f28c1). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 03:13:26 +00:00
e8f28c163c Merge pull request 'feat(kebab-tui): p9-fb-14 color theme module — role-based palette' (#70) from feat/p9-fb-14-theme into main 2026-05-03 03:12:58 +00:00
95ba7d5b39 review(p9-fb-14): 회차 1 nit 반영
- 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>
2026-05-03 03:12:38 +00:00
afb65702b6 feat(kebab-tui): p9-fb-14 color theme module — role-based palette
도그푸딩 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>
2026-05-03 03:09:53 +00:00
5b5d35852b Merge pull request 'chore(tasks): mark p9-fb-09 completed' (#69) from chore/p9-fb-09-status-completed into main 2026-05-03 02:36:45 +00:00
cb178fe846 chore(tasks): mark p9-fb-09 completed
PR #68 머지 (b8b3236). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:36:08 +00:00
b8b3236899 Merge pull request 'feat(kebab-tui): p9-fb-09 external editor return — terminal restore' (#68) from feat/p9-fb-09-editor into main 2026-05-03 02:35:44 +00:00
8c22e3792c review(p9-fb-09): 회차 1 nit 반영
- `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>
2026-05-03 02:35:10 +00:00
2c10cb7e7a feat(kebab-tui): p9-fb-09 external editor return — terminal restore
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>
2026-05-03 02:32:01 +00:00
58dc20210a Merge pull request 'chore(tasks): mark p9-fb-07 completed + ignore .claude/' (#67) from chore/p9-fb-07-status-completed into main
Reviewed-on: #67
2026-05-03 02:16:01 +00:00
984bfeb622 chore: untrack .claude/scheduled_tasks.lock
직전 커밋 91fcc2d 가 실수로 포함했던 Claude Code runtime lockfile 을
tracking 에서 제거. `.gitignore` 갱신 (7422d83) 으로 향후 재발 방지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:29:01 +00:00
7422d83ac4 chore(tasks): mark p9-fb-07 completed + ignore .claude/
PR #66 머지 (eac08ed). 머지 후 spec status 갱신.

같이 `.gitignore` 에 `.claude/` 추가 — Claude Code 가 워크스페이스
루트에 만드는 runtime 산출물 (`scheduled_tasks.lock` 등) 이 실수로
커밋되는 사고 방지. 직전 push 에 동일 파일이 한 번 들어갔으나 force
push 대신 같은 branch 의 새 커밋으로 정리.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:28:40 +00:00
91fcc2d98d chore(tasks): mark p9-fb-07 completed
PR #66 머지 (eac08ed). 머지 후 spec status 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:27:33 +00:00
eac08ed545 Merge pull request 'feat(kebab-normalize): p9-fb-07 markdown title fallback chain' (#66) from feat/p9-fb-07-title into main 2026-05-03 01:26:53 +00:00
28609d9eb9 review(p9-fb-07): 회차 1 nit 반영
- `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>
2026-05-03 01:26:26 +00:00
7a49c8a29b feat(kebab-normalize): p9-fb-07 markdown title fallback chain
`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>
2026-05-03 01:22:34 +00:00
fae9ca803e Merge pull request 'chore(tasks): mark p9-fb-20 completed' (#65) from chore/p9-fb-20-status-completed into main 2026-05-03 00:41:32 +00:00
bb555dbe37 chore(tasks): mark p9-fb-20 completed (PR #64 merged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:41:19 +00:00
c42b4bc467 Merge pull request 'feat(cli): kebab ask citation block (p9-fb-20)' (#64) from feat/p9-fb-20-citation into main 2026-05-03 00:40:47 +00:00
f80d5ef542 review(회차1): per-citation score 제거 + marker fallback 단순화
회차 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>
2026-05-03 00:40:10 +00:00
c1555f3c50 feat(cli): kebab ask citation block (p9-fb-20)
답변 출력 후 `근거:` 절 — `[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>
2026-05-03 00:37:47 +00:00
d3a90bdd7b Merge pull request 'chore(tasks): mark p9-fb-16 completed' (#63) from chore/p9-fb-16-status-completed into main 2026-05-03 00:04:20 +00:00
be6f5e02d0 chore(tasks): mark p9-fb-16 completed (PR #62 merged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 00:04:04 +00:00
d3b6cc9407 Merge pull request 'feat(tui): Ask conversation transcript UI (p9-fb-16)' (#62) from feat/p9-fb-16-tui-conv into main 2026-05-03 00:03:16 +00:00
6d5f39632f review(회차1): refusal yellow + Ctrl-L race fix
회차 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>
2026-05-03 00:02:43 +00:00
7ea7264f5d feat(tui): Ask conversation transcript UI (p9-fb-16)
Multi-turn ask pane. AskState 가 turns: Vec<Turn> + current_question
+ conversation_id + last_answer 로 재설계. answer area 가 transcript
형식 (Q1/A1, Q2/A2, ...) 로 갈음, 매 Enter 가 이전 turns 를 history
로 worker 에 전달 — RagPipeline::ask_with_history 호출.

신규 (kebab-tui::app):
- AskState 에 turns / current_question / conversation_id / last_answer
  4 field 추가. 기존 answer field 제거 (last_answer 가 갈음).

신규 (kebab-tui::ask):
- spawn_ask_worker: 첫 submit 시 conversation_id 자동 생성
  (conv_<unix_nanos_hex>), input → current_question, input clear.
  history = turns.clone(), turn_index = turns.len(). worker 가
  ask_with_history 호출 (kebab-app facade 가 _cancellable 통해
  RagPipeline::ask_with_history 까지 thread).
- poll_worker: Answer 받으면 Turn { question: current_question,
  answer, citations, created_at } 만들어 turns 에 push, last_answer
  도 보존.
- handle_key_ask: Ctrl-L 가 turns + conversation_id 초기화 (in-flight
  worker 는 그대로 finish — 결과는 새 conversation 의 stale turn 으로
  silently 폐기, 사용자 의도와 일치).
- render_answer: 모든 completed turns + (있으면) in-flight turn
  chronological 출력. Q/A 라벨 색상 구분 (Q cyan bold, A green bold).
  in-flight answer 는 ▍ cursor + dim. transcript title 에 turn count.
- render_status / render_citations_or_explain: s.last_answer 사용.

Test:
- 17 PASS (3 신규: ctrl_l_clears_conversation_state /
  render_transcript_shows_completed_turns_in_order /
  render_streaming_inflight_turn_appears_below_completed_turns).
- 기존 14 회귀 0 (기존 s.answer → s.last_answer + Turn fixture
  push).

README + HANDOFF: TUI 행에 multi-turn 동작 추가. spec status
planned → in_progress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:58:26 +00:00