docs/components/<group>/README.md 12 페이지 + 인덱스 작성. 각 그룹 페이지가 구성 crate 표 + 구조 mermaid + data flow mermaid + 주요 type/trait/함수 시그니처 + 외부 의존 + 핵심 결정 (HOTFIXES + spec 의 "왜" 통합) + 관련 spec/HOTFIXES 링크. 인덱스가 그룹 wiring 다이어그램 + 진입 가이드 보유. ARCHITECTURE.md 의 ASCII crate 의존 그래프를 mermaid flowchart 로 교체 (등가 정보, Gitea/GitHub 자동 렌더). docs/components/ 진입 링크 추가. 이 layer 는 contributor 향 — 사용자 향 grand picture 는 README.md 의 logical-architecture diagram 그대로 유지. 진척도는 HANDOFF.md, per-task spec 은 tasks/INDEX.md 가 기존대로 source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
App facade
kebab-app은 모든 UI binary (cli/tui/desktop) 가 의존하는 유일한 진입점. 도메인 type 만 반환 (wire envelope 은 UI 측 책임).*_with_configcompanion 패턴으로 explicit Config threading 보장.
구성 crate
| Crate | 역할 |
|---|---|
kebab-app |
high-level facade. 모든 pipeline crate 를 wire 해서 ingest / search / ask / list / inspect / doctor / reset / init 8개 op 노출. App lifecycle struct + *_with_config companion + IngestEvent streaming + cooperative cancel. |
구조
classDiagram
class App {
+open_with_config(cfg) Self
+embedder() Option~Arc~dyn Embedder~~
+vector() Option~Arc~LanceVectorStore~~
+llm() Arc~dyn LanguageModel~
+search(query) Vec~SearchHit~
+ask_with_session(session_id, query, opts) Answer
-config: Config
-sqlite: Arc~SqliteStore~
-embedder: OnceLock
-vector: OnceLock
-llm: OnceLock
-search_cache: Option~Mutex~LruCache~~
}
class FreeFns {
<<top-level pub fn>>
init_workspace(force)
ingest(scope, summary_only)
list_docs(filter)
inspect_doc(id) / inspect_chunk(id)
search(query)
ask(query, opts)
doctor()
}
class WithConfig {
<<#[doc(hidden)] pub fn>>
ingest_with_config
ingest_with_config_progress
ingest_with_config_cancellable
list_docs_with_config / inspect_*_with_config
search_with_config / search_uncached_with_config
ask_with_config / ask_with_session_with_config
doctor_with_config_path
}
class IngestEvent {
<<enum>>
Started{total}
AssetStarted{path}
AssetCompleted{counts}
Completed{counts}
Aborted{partial_counts}
}
class ResetReport {
scope: ResetScope
wiped: Vec~PathBuf~
}
class DoctorReport {
schema_version: u32
checks: Vec~DoctorCheck~
}
FreeFns ..> WithConfig : load_config + delegate
App ..> WithConfig : long-lived path
WithConfig ..> IngestEvent : stream channel
Data flow — kebab ingest (cancellable + progress)
flowchart LR
CLI["kebab ingest --json<br/>(또는 TUI worker)"]
Cfg["Config::load(--config)"]
App2["App::open_with_config<br/>(SQLite, lazy embedder/vector)"]
Walk["FsSourceConnector.scan<br/>(Source 그룹)"]
Loop["per asset 루프<br/>(cancel poll 매 iter)"]
Route["MediaType 라우팅<br/>md / pdf / image"]
Parse["Parse 그룹"]
Norm["Normalize+Chunk 그룹"]
Emb["Embed 그룹 (선택)"]
Store["Store 그룹<br/>two-phase upsert"]
Bump["bump_corpus_revision<br/>(p9-fb-19)"]
Event["IngestEvent::*<br/>→ Sender~IngestEvent~"]
Report["IngestReport<br/>(stage_counts, warnings)"]
Aborted["IngestEvent::Aborted<br/>partial commit 보존"]
CLI --> Cfg --> App2 --> Walk --> Loop --> Route --> Parse --> Norm --> Emb --> Store
Loop -.cancel=true.-> Aborted
Loop -.progress.-> Event
Store --> Bump --> Report
Data flow — kebab ask --session <id> (multi-turn)
flowchart LR
Q["query + session_id"]
Repo["ChatSessionRepo<br/>get_session / list_turns"]
History["Vec~Turn~<br/>(newest-first)"]
RAG["RagPipeline.ask_with_history"]
Append["append_turn<br/>(parent updated_at bump)"]
Out["Answer<br/>(conversation_id + turn_index)"]
Q --> Repo --> History --> RAG --> Append --> Out
Q -.first call.-> CreateSession["create_session<br/>title = 첫 question 40 chars"]
CreateSession --> Repo
주요 type / trait / 함수
Top-level free fn (사용자 향, XDG Config 자동 로드):
init_workspace(force: bool)—~/.config/kebab/config.toml+~/.local/share/kebab/생성. path policy comment 자동 prepend (p9-fb-05).ingest(scope, summary_only) -> IngestReport— workspace 전체 ingest.list_docs(filter) / inspect_doc(id) / inspect_chunk(id) / search(query) / ask(query, opts) / doctor()— 그대로.
*_with_config companion (#[doc(hidden)] pub fn, but 공식 API):
ingest_with_config(cfg, scope, summary_only)— 가장 단순.ingest_with_config_progress(cfg, scope, progress: Option<Sender<IngestEvent>>)— TTY 진행 표시 /--jsonline-delimited 용.ingest_with_config_cancellable(cfg, scope, progress, cancel: Option<Arc<AtomicBool>>)— 위 + cooperative cancel. asset loop iter 시작에서 poll → true 면 break +Aborted{partial_counts}+Ok(IngestReport)반환 (Err 아님). 부분 commit 보존.search_with_config / search_uncached_with_config— 후자는 LRU cache bypass (debug).ask_with_session_with_config(cfg, session_id, query, opts)— multi-turn (p9-fb-18).doctor_with_config_path(Option<&Path>)— config 경로 explicit.
App lifecycle (long-lived caller 향 — kebab-eval, future TUI session):
App::open_with_config(cfg) -> Result<Self>— SQLite open + migration. embedder/vector/llm 은 lazyOnceLock으로 첫 호출에서 build.App::embedder() -> Option<...>—provider == "none"또는dimensions == 0이면None(lexical-only fallback).App::ask_with_session(session_id, query, opts)— repo + RAG ask + 새 turn append 한 묶음 (p9-fb-18).App::search(query)— LRU cache lookup → miss 시search_uncached→ put. cache key =(query_norm, mode, k, snippet_chars, embedding_version, chunker_version, corpus_revision).
IngestEvent (kebab-app::ingest_progress):
Started { total }/AssetStarted { workspace_path }/AssetCompleted { counts }/Completed { counts }/Aborted { partial_counts }. terminal frame 후 sender drop.
ResetReport / ResetScope (kebab-app::reset):
--all/--data-only/--vector-only/--config-only(p9-fb-06).- TTY 가 아니면
--yes필수 (silent destruction 방어). --vector-only가 SQLiteembedding_records도 truncate (off-disk Lance dir wipe 시 orphan 방지).
외부 의존
- crate dep: 거의 모든 kebab-* (source-fs, parse-md/pdf/image, normalize, chunk, store-sqlite, store-vector, embed-local, llm-local, search, rag, config, core).
- 외부 lib:
lru(search cache),serde,anyhow,tracing,time,ctrlc(CLI signal). - 외부 서비스: 없음 (down-stream adapter 가 가져옴).
핵심 결정
-
Facade rule: UI binary 는
kebab-app만 import. 왜: store / llm / parse / search 직접 import 가 boundary 깨짐 → UI 가 SQLite 를 알면 swap 안 됨, ONNX 를 알면 cold start 비대화.kebab-app만 알게 하면 future MCP server / HTTP wrapper 도 같은 contract 위에 build. -
*_with_configcompanion = 공식 API (test seam 아님). 왜: top-levelingest()는Config::load(None)으로 XDG default 만 읽음 →kebab-cli --config /tmp/foo.toml가 silently bypass 되는 회귀 두 번 (HOTFIXES P3-5, P4-3).kebab-cli는 항상*_with_config호출 — 이 패턴이 spec literal 을 후행 강화.#[doc(hidden)]는 rustdoc 깨끗함만, public 그대로. -
App.{embedder, vector, llm}가OnceLocklazy + memoized. 왜:kebab list/kebab inspect/--mode lexical가 ONNX (~470 MB) + Lance reopen 비용 0 이어야 함. 매 CLI invocation 가 cold start 부담 = sub-second 가 다 망가짐. 첫 사용 시 build, 같은App재사용 시 재 build 안 함 (kebab-eval 50 query suite, TUI session). -
embedding 비활성 모드 (
provider = "none"또는dimensions = 0). 왜: 사용자 환경 (헤드리스 / OS 미지원) 에서 embedding 끄고 lexical-only 쓰는 escape hatch.App::embedder()가None반환 → search 가 mode=lexical 에 falls back. config-only switch. -
ingest_with_config*3개 함수 (vanilla / progress / cancellable). 왜: 호출 사이트 별 capability 다름. CLI 가 progress 만 필요 / TUI worker 가 progress + cancel 둘 다. 단일 함수에Option<Sender>+Option<AtomicBool>다 받게 하면 caller 시그니처 noisy. 3 layered: vanilla → progress wraps it (cancel=None) → cancellable wraps progress. -
Aborted=Ok(IngestReport),Err아님. 왜: cancel 은 사용자 의도. partial commit 보존 (다음 ingest idempotent 재개).Err로 처리하면 caller 가 cleanup 강요, partial 도 sweep 됨. wire 측에서IngestEvent::Abortedframe 으로 cancel signal 분명,IngestReport.partial_counts가 진행 상황 보유. -
App.search_cacheLRU +corpus_revisionsnapshot. 왜: TUI 의 매 keystroke debounced search 가 같은 query 반복. capacity 256 (~1.3 MB) cap. cache key 의corpus_revisionsnapshot 이 ingest commit 후 자동 invalidation — 사용자가 문서 추가하면 다음 search 가 새로 쿼리 (p9-fb-19). -
wire-schema envelope = UI 측 (
kebab-cli/wire.rs) 책임. 왜:kebab-app의 함수가 pure 도메인 type 만 반환 → kebab-tui 가 wire envelope 안 거치고 in-memory 직접 사용.--json출력은 cli 가*.v1envelope 으로 wrap.DoctorReport만 예외 — 자체schema_version보유 (도메인 측 동등 type 없음). -
multi-turn 세션 진입 =
App메서드 (ask_with_session). 왜: 세션 복구 + history 빌드 + ChatSessionRepo append 가 한 transaction. caller (CLI / TUI) 가 매번 free fn 으로 호출하면 매번App::open→ ONNX cold start.App위 메서드 = 한 번 open 후 N 번 ask.
관련 spec / HOTFIXES
- frozen 설계 §2.4a (ingest progress wire), §3.8 (Answer / Turn), §5.7a (chat sessions), §6.4 (defaults), §7 (facade), §8 (boundary), §10 (long-running ops), §11 (errors):
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md - task spec:
- app skeleton + ingest wiring:
tasks/p3/p3-5-app-wiring.md,tasks/p6/p6-4-image-ingest-wiring.md,tasks/p7/p7-3-pdf-ingest-wiring.md - reset:
tasks/p9/p9-fb-06-data-reset-command.md - ingest progress / cancel:
tasks/p9/p9-fb-03-tui-ingest-background.md,tasks/p9/p9-fb-04-ingest-cancellation.md - search cache:
tasks/p9/p9-fb-19-search-cache.md - chat session CLI:
tasks/p9/p9-fb-18-cli-ask-session-repl.md
- app skeleton + ingest wiring:
- HOTFIXES (P3-5/P4-3
--config누락 +*_with_config패턴, P7-3 storage UNIQUE bug, p9-fb-* 도그푸딩 후속):tasks/HOTFIXES.md