Files
kebab/docs/components/app-facade/README.md
th-kim0823 af8c162e09 docs(components): per-group contributor reference (12 그룹)
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>
2026-05-04 15:05:32 +09:00

11 KiB

App facade

kebab-app 은 모든 UI binary (cli/tui/desktop) 가 의존하는 유일한 진입점. 도메인 type 만 반환 (wire envelope 은 UI 측 책임). *_with_config companion 패턴으로 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 진행 표시 / --json line-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 은 lazy OnceLock 으로 첫 호출에서 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 가 SQLite embedding_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_config companion = 공식 API (test seam 아님). : top-level ingest()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}OnceLock lazy + 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::Aborted frame 으로 cancel signal 분명, IngestReport.partial_counts 가 진행 상황 보유.

  • App.search_cache LRU + corpus_revision snapshot. : TUI 의 매 keystroke debounced search 가 같은 query 반복. capacity 256 (~1.3 MB) cap. cache key 의 corpus_revision snapshot 이 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 가 *.v1 envelope 으로 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