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>
Eval
Golden query 회귀 평가. fixture YAML 로드 → kebab-app facade 통해 실행 → 결과 + 메트릭을 SQLite + per_query.jsonl 로 저장. 두 run 간 비교 (compare). LLM-as-judge 안 함 — rule-based
must_contain.
구성 crate
| Crate | 역할 |
|---|---|
kebab-eval |
golden fixture loader + run executor + 메트릭 computer + run-vs-run comparison + markdown 리포트. retrieval/embedding/LLM crate 직접 의존 금지 — 모두 kebab-app facade 통해서만 (P5-1 inheritance). |
구조
classDiagram
class GoldenQuery {
id: String
query: String
mode: SearchMode
k: usize
must_contain: Vec~String~
expected_doc_ids: Option~Vec~String~~
ask: bool
}
class QueryResult {
query_id
chunks_returned: u32
top_score
contains_all: bool
ask_grounded: Option~bool~
ask_refusal_reason
per_query_jsonl_path
}
class EvalRun {
run_id: String
started_at
finished_at
config_snapshot_json: String
results: Vec~QueryResult~
}
class AggregateMetrics {
recall_at_k: HashMap~k,f32~
precision_at_k: HashMap~k,f32~
no_hit_rate: f32
refusal_rate: f32
ask_grounded_rate: Option~f32~
}
class CompareReport {
baseline: EvalRun
candidate: EvalRun
per_query: Vec~QueryComparison~
kind: ComparisonKind
}
class ComparisonKind {
<<enum>>
Better
Same
Regressed
}
EvalRun --> QueryResult
EvalRun --> AggregateMetrics
CompareReport --> EvalRun
CompareReport --> QueryComparison
Data flow — eval run
flowchart LR
YAML["fixtures/golden_queries.yaml"]
Load["load_golden_set"]
QSet["Vec~GoldenQuery~"]
RunId["uuid v7 simple<br/>(timestamp-ordered)"]
Snapshot["5-version snapshot<br/>(parser/chunker/embedding/<br/>index/prompt_template)"]
Loop["per query 루프"]
Search["kebab_app::search_with_config"]
Ask["kebab_app::ask_with_config<br/>(query.ask == true 시)"]
MustContain["must_contain<br/>contains_all 검사"]
Result["QueryResult"]
Aggregate["compute_aggregate<br/>recall@k / precision@k /<br/>no_hit_rate / refusal_rate /<br/>ask_grounded_rate"]
SQLiteRow["eval_runs row +<br/>eval_query_results rows"]
Jsonl["runs_dir/<run_id>/<br/>per_query.jsonl"]
YAML --> Load --> QSet --> Loop
RunId --> Snapshot --> SQLiteRow
Loop --> Search --> Result
Loop --> Ask --> Result
Result --> MustContain --> Aggregate
Aggregate --> SQLiteRow
Result --> Jsonl
Data flow — compare two runs
flowchart LR
Base["baseline run_id"]
Cand["candidate run_id"]
LoadRows["eval_runs + eval_query_results<br/>rows 로드"]
Diff["per query 비교<br/>(contains_all flip,<br/>top_score delta,<br/>refusal flip)"]
Kind["ComparisonKind<br/>(Better / Same / Regressed)"]
MD["render_report_md<br/>(human-friendly)"]
Base --> LoadRows
Cand --> LoadRows --> Diff --> Kind --> MD
주요 type / trait / 함수
Loader (kebab-eval::loader):
load_golden_set(path: &Path) -> Result<Vec<GoldenQuery>>—serde_yaml위 YAML 파싱.GoldenQuery { id, query, mode, k, must_contain, expected_doc_ids, ask }— fixture 한 entry.must_contain가 case-sensitive substring 검사.
Runner (kebab-eval::runner):
run_eval(opts: EvalRunOpts) -> Result<EvalRun>/run_eval_with_config(cfg, opts) -> Result<EvalRun>.EvalRunOpts { golden_path, ask_enabled, k_override, ... }.- 각 query →
kebab_app::search_with_config호출 (모든 retrieval 은 facade 통해).ask=true시 추가로ask_with_config. run_id= UUID v7 simple (timestamp-ordered, lowercase hex).config_snapshot_json= 5 version 모두 (parser_version/chunker_version/embedding_version/index_version/prompt_template_version) 한 번에 capture → 후일 compare 가 정확히 같은 environment 인지 검증.runs_dir/<run_id>/per_query.jsonl로 raw search hit + answer 저장 — SQLite 의eval_query_results는 메트릭 + 요약만, full payload 는 JSONL.
Metrics (kebab-eval::metrics):
AggregateMetrics { recall_at_k, precision_at_k, no_hit_rate, refusal_rate, ask_grounded_rate }.TOP_K_VARIANTS— 표준 k 값 set (1, 3, 5, 10, ...). 한 run 에서 multiple k 측정.compute_aggregate(run: &EvalRun) -> AggregateMetrics/_with_configcompanion.store_aggregate(...)— SQLiteeval_runs.aggregate_json컬럼에 저장.- LLM-as-judge 안 함.
ask_grounded_rate는Answer.refusal_reason.is_none() && answer.citations.len() > 0rule.
Compare (kebab-eval::compare):
compare_runs(baseline_id, candidate_id) -> Result<CompareReport>/_with_config.CompareOpts { include_unchanged, k_focus }.QueryComparison { query_id, baseline_result, candidate_result, deltas: ContainsFlipped/TopScoreDelta/RefusalFlipped }.ComparisonKind { Better, Same, Regressed }— overall verdict.render_report_md(&CompareReport) -> String— markdown 본문 (PR 리뷰 첨부 용도).
외부 의존
- crate dep:
kebab-core+kebab-config+kebab-app(facade only) +kebab-store-sqlite(SQLite 직접 read/write —eval_runs/eval_query_results측). retrieval / embedding / LLM crate 직접 import 금지. - 외부 lib:
serde_yaml(golden YAML),serde_json,uuid(v7),time,tracing,anyhow. - 외부 서비스: 없음 (facade 가 가져옴).
핵심 결정
-
runner 가
kebab-appfacade 통해서만 실행 (직접 retrieval 금지). 왜: facade rule (P5-1 inheritance). retrieval/embedding/LLM 의 swap 가 eval 의 contract 깨면 안 됨. 같은 facade 호출이 production 에서도 → eval 결과가 production 동작과 등가. -
rule-based
must_contain, LLM-as-judge 거부. 왜: LLM judge 가 stochastic + 비용 발생 + 모델 swap 시 baseline 회귀.must_containsubstring 검사가 deterministic + 무료 + 사용자가 fixture 작성 시 이미 의도 명시. spec §11 의 비-목표 명시. -
5-version
config_snapshot_json. 왜: 두 eval run 비교 시 environment drift 잡음. parser_version / chunker_version 한 단계라도 다르면 비교 무의미 (다른 chunk_id, 다른 retrieval 결과). compare 가 mismatch 시 경고. -
per_query.jsonl off-disk + SQLite metrics-only. 왜: SQLite row 가 raw search hit + answer 본문 다 보관하면 row 가 거대해짐 + FTS5 인덱스 노이즈. JSONL 은 개별 파일 → 디스크 저렴, append-only 안전, jq / fzf 로 ad-hoc 분석 가능. SQLite 는 메트릭 + 요약만.
-
UUID v7 run_id. 왜: timestamp 순 정렬 (v4 random 은 정렬 안 됨) + collision-free + lowercase hex.
runs_dir/<run_id>/가 자연 정렬 시 시간 순. -
ask_grounded_rate=refusal == None && citations > 0. 왜: "grounded" 정의가 spec §3.8 — refusal 아니고 citation 보유. LLM 의 답변 품질 (hallucination 여부) 평가 LLM-judge 없이는 불가능, 가까운 proxy 가 grounded rate. -
compare 의
ComparisonKindoverall verdict. 왜: PR review 가 "regressed 인가?" 한 줄 답이 핵심. per-query delta 모두 봐야 verdict 가 나오면 leverage 안 남.Better/Same/Regressed단일 verdict + 세부 delta 가 backing. -
eval 자체 cancellable 안 함. 왜: 50-query suite 가 ~5 분. 중도 cancel 시 partial run 가 baseline 으로 쓰이면 회귀 검출 부정. CLI Ctrl-C 면 hard exit (소실 OK), partial state 저장 안 함.
관련 spec / HOTFIXES
- frozen 설계 §5.7 (eval_runs / eval_query_results), §6.3 (runs_dir), §11 (비-목표 = LLM-as-judge 금지), §9 (5-version cascade):
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md - task spec:
- runner:
tasks/p5/p5-1-eval-runner.md - metrics + compare:
tasks/p5/p5-2-eval-metrics.md
- runner:
- HOTFIXES (P5-1 facade-inheritance 결정, P5-2 metric definition tweaks):
tasks/HOTFIXES.md