Wire kebab_parse_code::is_generated_file and is_oversized into
FsSourceConnector::scan_with_skips. Files that pass gitignore/builtin/
kebabignore matching are now checked for generated-file markers
(config-gated via ingest.code.skip_generated_header) and byte/line caps
(ingest.code.max_file_bytes / max_file_lines). FsScanSkips gains
skipped_generated + skipped_size_exceeded counters; kebab-app threads
them into IngestReport. Also fixes a pre-existing clippy::derivable_impls
warning in IngestCfg. Three new connector tests cover all three paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add IngestCfg + IngestCodeCfg structs with serde defaults and embed
ingest: IngestCfg into the top-level Config. Existing configs without
an [ingest] section continue to load unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 1: Add multilingual-e5-large arm to kebab-embed-local::resolve_model with tests for 1024-dim variants and error cases.
Task 2: Flip kebab-config defaults from e5-small (384-dim) to e5-large (1024-dim) across defaults(), test assertions, and TOML template.
All tests pass; clippy clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- spec: add one-line cross-link to HOTFIXES entry per CLAUDE.md
Spec-contract policy
- HOTFIXES: rename heading from "fb-32" to "p9-fb-32" matching
the rest of the file's full-ID convention
- config: defensive assert before string-replace in negative TOML
test guards against default-value drift causing unhelpful unwrap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dirs::config_dir() and dirs::data_dir() both return ~/Library/Application Support
on macOS, so data_dir == config parent dir. ResetScope::DataOnly removes data_dir
and silently deletes config.toml along with it.
Fix: bypass dirs crate fallback for config/data/cache dirs; use
$HOME/.config, $HOME/.local/share, $HOME/.cache directly (XDG standard).
xdg_state_dir already used this pattern. dirs::home_dir() still used for
portability.
Migration: Config::load(None) auto-copies legacy ~/Library/Application
Support/kebab/config.toml to the new ~/.config/kebab/ on first run and
prints a migration notice to stderr.
Replace `read failed: {e}` / `parse failed: {e}` with the underscore-
slugged `read_failed:` / `parse_failed:` prefixes so kebab-cli's
error_classify (Task 8) and the error.v1 JSON Schema (Task 14) can
treat the prefix as a stable wire contract while leaving the OS /
toml-crate detail in the suffix as free-form context.
Also add the symmetric `cause` non-empty assertion to the malformed-TOML
test so a regression that empties `cause` on the parse path would be
caught.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps every error path in `Config::from_file` (read failure, TOML parse,
validation) so downstream callers can `downcast_ref::<ConfigInvalid>()`
to build the `error.v1` wire record. kebab-app re-exports the type via
its `error_signal` module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove `pub include: Vec<String>` from `WorkspaceCfg` struct (denylist-only model).
- Drop `include: vec!["**/*.md"]` from `Config::defaults()`.
- Add `from_file` deprecation probe: raw `toml::Value` scan fires a
one-shot `tracing::warn!` (via `OnceLock`) when an old config still
carries `workspace.include = [...]`. serde ignores the unknown field
cleanly (no `deny_unknown_fields`).
- Compile-fix `kebab-cli` (main.rs:329) and `kebab-tui`
(ingest_progress.rs:39): replace `cfg.workspace.include.clone()` with
`Vec::new()` (Task 2 will switch to `..Default::default()`).
- Two new tests: `legacy_include_field_is_ignored_silently` (backward
compat round-trip) + `workspace_cfg_has_only_root_and_exclude_fields`
(exhaustive destructure — compile-time guard against re-introduction).
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>
도그푸딩 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-02): 텍스트 LLM 기본을 gemma4 계열로
통일. OCR/caption 어댑터 (P6-2/P6-3) 가 이미 gemma4:e4b 사용 중 —
사용자가 한 family 만 pull 하면 ingest + ask 모두 작동.
같이 발견된 ~ expansion 불일치:
- kebab-source-fs::connector 는 expand_tilde 사용 (walk 정상)
- kebab-app::ingest_one_image_asset / ingest_one_pdf_asset 은 직접
PathBuf::from → ~ 미확장 → ExtractContext 에 ~/KnowledgeBase
그대로 전달
- kebab-tui::search::handle_key_search 의 editor jump 도 동일 →
의미 없는 경로 spawn
Fix:
- Config::defaults().models.llm.model = \"gemma4:e4b\". OCR/caption
family 통일 코멘트 추가.
- kebab-app 의 image / pdf 분기 두 곳 모두 expand_tilde 호출.
- kebab-tui::search jump 가 kebab_config::expand_path(.., \"\") 사용
(expand_path 는 ~ / ${XDG_DATA_HOME} / {data_dir} 모두 처리하는
정식 helper).
Caveat: kebab-app::expand_tilde 와 kebab-config::expand_path 가 별도
정의. 통합은 P+ task.
Docs (sync rule):
- README 사전 요구 절: gemma4:e4b 기본 + 더 큰 variant override 안내.
- docs/ARCHITECTURE 핵심 결정 표: LLM default qwen2.5:7b-instruct →
gemma4:e4b.
- docs/SMOKE: ollama pull 예시 + KEBAB_MODELS_LLM_MODEL env 예시
qwen2.5:32b → gemma4:26b.
- HOTFIXES: 새 entry (\"Config defaults: LLM = gemma4:e4b + workspace.root
tilde expansion\").
- Memory: project_llm_default.md 신설, MEMORY.md 인덱스 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- crates/kebab-config/src/lib.rs:
• `OcrCfg.endpoint: String` (\"\" sentinel) → `Option<String>` 으로 교체.
`#[serde(default)]` 적용. `KEBAB_IMAGE_OCR_ENDPOINT=\"\"` (빈 값) 도
None 으로 매핑하는 분기 추가.
• 신규 회귀 테스트 `image_ocr_endpoint_empty_env_value_is_none`.
- crates/kebab-parse-image/src/ocr.rs:
• `OllamaVisionOcr::new` 의 endpoint fallback 로직을 새 `Option<String>`
스키마에 맞춰 정리 (`as_deref` + match).
• `OllamaGenerateResponse` 의 dead `_other: HashMap<String, Value>` 필드
제거. `serde_json::Value` import 도 같이 정리.
• `OllamaGenerateRequest.images: Vec<&'a str>` → `[&'a str; 1]`
(호출당 vec! 알로케이션 제거, multi-image 는 OcrEngine trait 가
단일 이미지를 받으므로 OOS).
• `downscale_to_long_edge` 단일-디코드로 리팩터. PNG passthrough
hot path 보존 (header sniff 만으로 분기), 그 외 모든 경로는
decode 1회 + (필요 시) resize + PNG re-encode 1회로 통일.
• `pub fn max_pixels(&self) -> u32` accessor 추가 — clamp 결과
검증 용 (단순 inspector).
- crates/kebab-parse-image/tests/ocr.rs:
• `cfg_for_endpoint` / 통합 테스트가 `Some(endpoint)` 형태로 갱신.
• `from_parts_clamps_max_pixels_into_legal_range` 가 새 accessor
로 실제 클램프 결과 (256 / 4096 / 1024) 를 검증하도록 강화.
• 통합 테스트가 폰트 부재 시 panic 대신 skip 하도록 분기.
- crates/kebab-parse-image/tests/common/mod.rs:
• `hello_world_png` 가 `anyhow::Result<Vec<u8>>` 반환하도록 변경.
expect(\"DejaVu Sans Bold required\") 메시지를 \"only the opt-in
OCR integration fixture needs this font\" 로 의도 명확화.
cargo test -p kebab-parse-image — 28 pass + 1 ignored.
cargo test -p kebab-config — 21 pass (+1 회귀).
cargo clippy --workspace --all-targets -- -D warnings — pass.
Reviewer-suggested workspace.dependencies 통합 (reqwest / base64) 은
P6-3 와 함께 처리할 수 있도록 follow-up 으로 두고 본 PR scope 에서
제외 (회차 1 본문에서 명시).
- 새 모듈 `crates/kebab-parse-image/src/ocr.rs` 추가. spec 의 `OcrEngine`
trait 그대로 + `OllamaVisionOcr` default 구현 + `apply_ocr` 헬퍼.
- `OllamaVisionOcr`: `<endpoint>/api/generate` 비스트리밍 호출,
`images: [base64]` 필드로 이미지 전달, 프롬프트는 언어 힌트
+ 화이트리스트 언어 목록 포함. 응답 prose 를 `OcrText.joined` 로,
prepared image 전체 영역 단일 region (confidence 1.0) 으로 wrap.
기본 모델 `gemma4:e4b`. endpoint 비어 있으면 `models.llm.endpoint`
로 fallback.
- 이미지 전처리: long-edge `config.image.ocr.max_pixels` (기본 1600,
256~4096 클램프) 초과 시 PNG 로 재인코딩 (image::imageops::resize,
Triangle filter). PNG 입력이 max 이내면 zero-copy passthrough.
- `apply_ocr` 는 OCR 성공 시 block.ocr 를 Some 으로 채우고
ProvenanceKind::OcrApplied 이벤트 추가. 실패 시 block.ocr 는
None 그대로 + provenance 미기록 (부분 상태 누출 금지).
- `kebab-config`: 새 `ImageCfg.ocr: OcrCfg` 블록 (enabled/engine/model
/endpoint/languages/max_pixels). `#[serde(default)]` 로 pre-P6
TOML 호환. `KEBAB_IMAGE_OCR_*` 환경변수 5종 추가.
## Spec deviation
원래 P6-2 spec 은 Tesseract 를 default OCR 엔진으로 지정했으나, dev /
CI 호스트에서 `libtesseract-dev` 시스템 패키지 설치를 피하려고
Ollama-vision 으로 default 를 교체. `OcrEngine` trait 추상화는 spec
그대로 보존 — Tesseract / Apple Vision / PaddleOCR 어댑터는 같은
trait 으로 추후 feature-gate 추가 가능. 자세한 내역은
`tasks/HOTFIXES.md` 2026-05-02 항목 참조.
Trust 측면: vision LM 은 hallucinate 가능. `OcrText.engine = "ollama-vision"`
필드로 consumer 가 엔진 별 신뢰 분기 가능.
## 테스트
- 신규 (`tests/ocr.rs`, 8 + 1 ignored):
- 200 happy → OcrText 디코딩 (joined / engine / engine_version /
region count / bbox / confidence)
- 빈 응답 → 빈 regions
- 5xx → Err with status + body 포함
- 200 error envelope → Err
- apply_ocr → block.ocr Some + Provenance OcrApplied 1건
- apply_ocr error → block.ocr None 유지 + events 미기록
- 4000×3000 PNG → max_pixels=1024 까지 다운스케일, aspect ratio 보존
- from_parts max_pixels 클램프
- opt-in `KEBAB_OCR_INTEGRATION=1` 통합 (실제 192.168.0.47 Ollama
`gemma4:e4b` 로 \"Hello World 2026\" 전사 검증 완료)
- 신규 (`src/ocr.rs` unit): truncate, build_prompt 언어/힌트 처리
- `kebab-config` 테스트 +3: defaults, env override, pre-P6 TOML 호환
전체: `cargo test -p kebab-parse-image` 28 pass + 1 ignored,
`cargo test -p kebab-config` 20 pass,
`cargo clippy --workspace --all-targets -- -D warnings` pass.
contract: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
sections: §3.4 ImageRefBlock.ocr, §3.7a OcrText / OcrRegion, §9.1 OCR
vs caption provenance.