PR-7 (v0.18 dogfood probe-first) 머지 후 PR-5 의 test `ask_tool_routes_multi_hop_true_to_decompose_first` 가 stale empty-KB contract 로 deterministic fail. test-only fix — production code 0 touch.
- `minimal_config`: `score_gate = 0.0` (probe 의 second gate `top_score < score_gate` 우회, test config isolation).
- fixture `workspace_root/note.md`: "This note is about a compound containing X and Y in detail." — build_match_string 의 token_and branch (FTS5 implicit-AND) 가 `compound` + `about` + `and` 셋 다 매칭 필요. empirical SQLite REPL (V007 trigram DDL) 로 1 hit 확정.
- 기존 assertion 보존, single-pass branch 도 query "anything" 으로 fixture 미매칭 → NoChunks refusal 유지.
- 신규 `_multi_hop_short_circuits_when_probe_empty` test (REQUIRED — round-1 critic HIGH + verifier 격상): probe-empty short-circuit 의 MCP-layer wire shape pin (kebab-rag::multi_hop_empty_probe_pool_refuses_before_any_llm_call 은 RAG-layer 만 pin, MCP-layer 안전망 부재).
- module doc 갱신: 두 test 가 각각 pin 하는 contract enumerate. inline 주석 (line 94-101) 도 새 contract 정합.
- HOTFIXES.md 신규 dated entry \`## 2026-05-26 — HOTFIX #15 ...\` (date-top convention).
검증: cargo test --workspace -j 1 — 회귀 0 (known flaky 1 → 0). cargo clippy --workspace --all-targets -j 1 -- -D warnings clean.
Wire / behavior / version cascade: 0.
Refs: docs/superpowers/specs/2026-05-26-hotfix-15-mcp-ask-multi-hop-flaky-spec.md (review 3 rounds APPROVE)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.18.0 cut PR. fb-41 multi-hop RAG + NLI verification 의 user-visible surface (PR #176-180) + post-PR9 cleanup/refactor (PR #181) ship 마무리.
## 변경 사항
### Version
- workspace `Cargo.toml`: 0.17.2 → 0.18.0. Cargo.lock 자동 cascade (24 kebab-* crate 모두 0.18.0).
### Frozen design contract
- `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md`:
- §3.8 RAG types — RefusalReason 에 NliVerificationFailed + NliModelUnavailable + MultiHopDecomposeFailed 추가 + Multi-hop RAG + NLI verification 의 ask_multi_hop facade + step 8.5 NLI hook + HopRecord / VerificationSummary 명시.
- §9 versioning rules 표 — nli_model_version row 신규 (선택 — v0.19+ second adapter 시 wire surface candidate).
### Status transitions
- `docs/superpowers/specs/2026-05-25-p9-fb-41-finalize-spec.md`: status approved-by-team → completed.
- `docs/superpowers/plans/2026-05-25-p9-fb-41-finalize-plan.md`: status approved-by-team → completed (spec_status 도).
### User-facing docs
- `README.md`: 명령 표의 `kebab ask` row 에 `--multi-hop` flag + NLI 옵션 안내 한 단락 (mDeBERTa-v3 XNLI 280 MB 자동 다운로드 / RAM peak ~7-8 GB / threshold tuning 0.5 prod / 0.0 disable).
- `docs/SMOKE.md`: `[rag] nli_threshold = 0.0` config 예시 + 활성화 절차 + first-run download + RAM 권장 inline 안내.
### Handoff + dashboard
- `HANDOFF.md`: 한 줄 요약 의 현재 version 0.17.2 → 0.18.0. v0.18.0 cut entry 추가 (fb-41 multi-hop + NLI + cleanup ship). Component 카운트 단락에 fb-41 PR-9 의 kebab-nli + ask_multi_hop 추가 명시. 머지 후 결정 절 맨 위에 v0.18.0 fb-41 entry 신규.
- `tasks/INDEX.md`: p9-fb-41 ⏳ → ✅ 머지 (v0.18.0). v0.18.0 subsection 신규 — PR #176-181 의 6 sub-PR + cleanup 각 한 줄 요약.
## 비범위 / 별 작업
- HOTFIXES.md 의 fb-41 entry 는 이미 PR #180 (PR-9d closure) 에서 작성 완료 — 본 cut PR 에서 추가 anchor 불필요.
- SKILL.md 의 v0.18+ NLI 안내는 이미 PR-9c-2 에서 inline 추가 완료.
## 검증
- `cargo check --workspace -j 1` 통과 (모든 24 crate v0.18.0 확인).
- frozen design 의 RefusalReason enum 확장이 kebab-core 의 production code 와 정합 (PR-9c-1 시점부터 동일 variants 있음).
Wire 영향: 없음 (additive minor 는 PR-9c-1 에서 이미 ship, 본 commit 은 documentation cascade only).
Behavior 영향: 없음.
머지 후 `gitea-release v0.18.0` 으로 tag + release notes 작성.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR-8 baseline 의 S1/S10 latency 추정값 (~150s, ~80s) 이 부정확. `results/s1-multihop.json` + `results/s10-multihop.json` 가 실제로 614s / 589s (`jq '.usage.latency_ms'` 측정) — *PR-8 시점 baseline 이 아닌 더 이전 timeline*. S7 만 `results/post-pr8/` 에 retest 보존되어 비교 의미 있음 (158s baseline → PR-9 241s with NLI first-run download).
SUMMARY.md 의 latency 표를 정정 — S1/S10 의 *동일 시점 baseline 부재* 명시 + S7 의 단일 비교만 의미 있음 caveat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- N1: fetch 의 cache-hit 검사 경로가 실제로는 download 트리거 (ApiRepo::get 가 cache miss 시 download 후 path 반환). log 의 "NLI artifact cache hit" 가 *방금 download 한 직후* 출력 — misleading. hf_hub::Cache::new(cache_dir).repo(repo).get(filename).is_some() 로 변경 — Cache::get 은 fs lookup only, 네트워크 안 탐. actual download 횟수는 변화 없음 (1번), log accuracy 만 개선.
- N2: new_succeeds_on_default_config / score_empty_hypothesis_returns_err 가 XDG 실 디렉토리 (`~/.local/share/kebab/models/nli/...`) 를 create_dir_all → test pollution. tempdir_config() 헬퍼 추가 — TempDir 으로 storage.data_dir override, model_dir 는 `{data_dir}/models` 그대로 두어 expand_path 의 substitution 검증도 유지.
cargo test -p kebab-nli -j 1 → 6 passed / 0 failed (unit) + 5 ignored (integration, manual).
cargo clippy -p kebab-nli --all-targets -j 1 -- -D warnings clean.
inference.rs 미수정 → manual --ignored smoke 결과 (5/5 PASS) 그대로 유효.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
기존 expectation `entailment < 0.3` 가 너무 strict — mDeBERTa-v3 multilingual NLI 가 두 caffeine 사실 (premise: "Caffeine is a stimulant.", hypothesis: "The chemical formula of caffeine is C8H10N4O2.") 의 *neutral* 을 0.53 으로, entailment 를 0.43 으로 판단함 (서로 entail 안 하지만 모순도 아님 = 정확히 neutral).
spec §3 PR-9b 의 "entailment 낮음 — neutral/contradiction 이 winning channel" 의 *spirit* 은 *neutral 이 max* 임. expectation 을 `s.neutral > s.entailment && s.neutral > s.contradiction` 로 변경.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- OnnxNliVerifier fields: model_id, cache_dir (XDG model_dir/nli/<sanitized>), session/tokenizer OnceLock.
- new(): eager cache_dir stamp만 — actual model download + Session::commit_from_file 는 첫 score 호출 시 ensure_loaded() 가 lazy 수행.
- score(): ensure_loaded → tokenizer.encode(pair, OnlyFirst truncation max_length=512) → ndarray Array2<i64> → ort::Session::run → logits[1,3] → NliScores::from_xnli_logits.
- empty hypothesis edge: defense-in-depth bail (spec §2.3 의 caller-side skip 외 추가).
- sanitize_model_id helper: "/" → "_".
- 5 #[ignore] integration tests (EN self-entailment, EN unrelated, KR entailment, long premise truncation, empty hypothesis err) — manual smoke 가 PR description 첨부.
Cargo.toml: `download-binaries` feature 를 kebab-nli 의 ort dep 에 활성화 (PR-9b prep commit 의 후속). 단독 `cargo test -p kebab-nli` 의 per-crate feature 유니온은 fastembed 없이 ort/download-binaries 가 OFF 되어 ort-sys link 가 실패 — kebab-nli 측에서 명시적으로 켜 줘야 standalone build 가 ONNX 런타임 link 됨. workspace 전체 빌드에서는 fastembed 의 동일 opt-in 과 union 되어 부작용 없음.
Verification:
- cargo test -p kebab-nli -j 1 — PR-9a 의 6 unit pass (`score_returns_err_in_skeleton` → `score_empty_hypothesis_returns_err` 로 stub→실 path 갱신, 갯수 유지).
- cargo clippy -p kebab-nli --all-targets -- -D warnings clean.
- cargo build --workspace -j 1 — 회귀 0.
- Manual --ignored smoke 결과 PR body 첨부.
Wire 영향: 없음 (crate-internal).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR-9a 의 workspace.dependencies 만 declared 였던 5 crate 의존을 kebab-nli/Cargo.toml 에 활성화. PR-9b 의 OnnxNliVerifier 실 구현이 본 commit 위에서 빌드 가능.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- lib.rs::NliScores::faithfulness doc 의 `rag.nli_faithfulness_min` → `rag.nli_threshold` (spec §2.5/§2.6 의 실 config knob 이름 정합).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HOTFIXES.md 의 fb-41 entry 에 *post-PR-7 dogfood retest + PR-8 partial
mitigation* sub-section 추가 + *PR-9 NLI plan* anchor + 사용자 영향
절 갱신. config.rs 의 doc reference 가 정확한 entry sub-section
가리키도록 조정 — dangling reference 해소.
검증
- `cargo test -p kebab-config -j 1` — 모든 test 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.18 cut 전 fb-41 multi-hop RAG **layered defense** — PR-7 의 pre-decompose
probe gate 위에 추가 safety. PR-7 의 fix 만으로는 hybrid mode 의 RRF
top_score 가 gate 통과 시 (도그푸딩 S7 의 caffeine query) hallucination
여전히 발생 — synthesize 단계 자체의 safety 보강 필요.
**중요**: 본 PR 만으로는 S7 hallucination 완전 차단 안 됨 (gemma3:4b 의
prompt-following 한계 — 추가 dogfood S7 retest 에서 확인). 진짜 fix 는
PR-9 (NLI-based post-synthesis verification). PR-8 은 그 사이의 *partial
mitigation + safety in depth* — latency 4× 개선 (614s → 158s) + future
larger LLM 용 prompt rule.
설계: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
계획: /build/cache/dogfood-v018/results/PR-9-DESIGN.md (사용자 결정 후
spec/plan 으로 promotion)
## 변경
- `crates/kebab-config/src/lib.rs`:
- `RagCfg::multi_hop_max_pool_chunks` default **30 → 15**.
- rationale doc — gemma3:4b 가 30-chunk large prompt 에서 citation
rule 잃는 측정 결과.
- 2 unit test (`default_*` rename + `legacy_*` assert) 갱신.
- `crates/kebab-rag/src/pipeline.rs`:
- `MULTI_HOP_SYNTHESIZE_SYSTEM_PROMPT` 에 **답하기 전 self-check** rule
추가 — "[원본 질문] 의 핵심 entity (고유명사, 화학식, 수치 단위,
코드명, 약자) 가 [근거] 본문에 literal 으로 등장하지 않으면 다른
entity 의 정보로 답을 합성하지 말고 '근거가 부족하다' 답한다". example
(caffeine + Adam optimizer chunk) 도 명시.
## 도그푸딩 결과 (retest with PR-7 + PR-8)
| query | path | grounded | latency | answer |
|---|---|---|---|---|
| caffeine formula | single-pass | false (LlmSelfJudge) | 30s | "근거가 부족하다" ✓ |
| caffeine formula | multi-hop pre-fix | true ✗ | 141s | hallucination |
| caffeine formula | multi-hop PR-7 | true ✗ | 143s | hallucination (probe gate top_score 0.5 > 0.30) |
| caffeine formula | multi-hop PR-8 | true ✗ | **158s** | hallucination (LLM 가 새 rule 무시) — **latency 4× 개선** |
PR-8 의 부분 성과:
- pool 30→15 로 synthesize prompt size ↓ → latency 614s → 158s.
- prompt rule 은 future larger LLM (gemma2:9b, qwen2.5:7b 등) 에서 가치 ↑.
PR-8 의 한계:
- gemma3:4b 의 prompt-following 한계 — strong rule 도 무시하고 다른 entity
chunk (Adam optimizer formula) 의 본문을 caffeine 화학식 출처로 인용.
- LLM-self-judge 기반 safety 의 ceiling.
## 진짜 fix → PR-9 (별 PR)
학계 / industry 표준 검색 결과 (Self-RAG, CRAG, Auto-GDA, MedTrust-RAG):
deterministic post-synthesis verification 이 정답 path. **NLI-based
groundedness check** — mDeBERTa-v3-base-xnli (280 MB multilingual) ONNX
model 이 (premise=packed_chunks, hypothesis=answer) entailment 검사. score
< 0.5 면 refuse. PR-8 위에 layered defense.
## 검증
- `cargo test -p kebab-config -p kebab-rag -j 1` — 모든 test 통과
(config default test 2개 갱신, rag tests 영향 없음).
- `cargo clippy -p kebab-config -p kebab-rag --all-targets -j 1 --
-D warnings` clean.
- 단일 crate 직렬 build (16 GB RAM 제약).
- S7 dogfood retest — hallucination 여전 (PR 본문에 정직 명시).
## 변경 없음
- Wire schema — additive (config knob default 만 변경).
- PR-7 의 probe gate — 그대로 작동 (gate 통과 시 PR-8 의 추가 safety
layer).
- 다른 도그푸딩 P1 항목 (citation 일관성, binary path) — 별 PR.
## 다음
- **PR-9a/b/c**: NLI-based post-synthesis verification — 진짜 fix.
- PR-9 머지 후 dogfood S7 재검증 (예상: refuse + nli_score < 0.5).
- v0.18.0 cut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ask_multi_hop` 의 probe_hits 가 gate 검사 후 throw away 되는 의도
명시 — pool 초기값으로 재사용 안 하는 *invariant clarity* rationale 을
코드 안에 doc. 향후 retrieve cost 가 multi-hop bottleneck 이 될 경우
재검토 hint 도 함께.
검증
- `cargo test -p kebab-rag -j 1 --test multi_hop` 10 모두 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v0.18 cut 전 fb-41 multi-hop RAG 도그푸딩에서 발견된 **safety regression**
fix. 자세한 도그푸딩 결과는 `tasks/HOTFIXES.md` 의 2026-05-25 fb-41
pre-v0.18 entry + `/build/cache/dogfood-v018/results/SUMMARY.md` 참조.
## 문제 (S7)
Query: `What is the chemical formula of caffeine?` (KB 에 없는 fact).
- Single-pass `kebab ask`: retrieve top score 가 default `rag.score_gate
= 0.30` 미만 → `refuse_score_gate` → 안전한 refusal.
- Multi-hop `kebab ask --multi-hop`: **`grounded = true`**, 본문
`"카페인의 화학식은 C₉H₁₅N₃O 입니다 [#6]"` (hallucination — 실제
C₈H₁₀N₄O₂) + `[#6]` 가 Adam optimizer chunk 의 `g_t = ∂L/∂θ_i` 본문을
인용 (시각적 short structured token 매칭 trigger).
원인: `ask_multi_hop` 의 score-gate 검사가 *pool 의 top_score* 만 봤다.
multi-hop 의 pool 은 5 sub-queries 의 union — 한 sub-query 의 top score
가 gate 위면 다른 chunks 가 원본 query 와 무관해도 gate 통과 + synth →
LLM hallucinate.
## Fix
`ask_multi_hop` entry 에 **pre-decompose probe** 추가:
1. *원본 query* 로 retrieve 한 번 (LLM call 0회, ~ms).
2. probe empty → `refuse_no_chunks(None)` (decompose 안 함, hops=None).
3. probe top_score < gate → `refuse_score_gate(None)` (decompose 안 함).
4. probe pass → 기존 decompose / decide / synthesize flow 그대로.
Multi-hop 의 safety floor 가 single-pass 와 정확히 일치 — multi-hop 은
*원본 query 가 이미 KB 범위 내* 일 때만 cross-doc reasoning 추가.
비용: 한 번의 retrieve (수 ms), LLM call 없음. multi-hop 의 LLM-dominated
latency 대비 무시 가능.
## Tests
신규 3 회귀 핀 (`crates/kebab-rag/tests/multi_hop.rs`):
- `multi_hop_below_probe_gate_refuses_before_any_llm_call` — **S7 직접
회귀 핀**. low-score chunk + empty LM script → score_gate refusal, LM
calls 0회, hops=None. fix revert 시 즉시 panic.
- `multi_hop_empty_probe_pool_refuses_before_any_llm_call` — empty
retrieve 시 NoChunks refusal, LM calls 0회.
- `multi_hop_above_probe_gate_proceeds_to_decompose` — probe pass 시
full multi-hop flow 정상 (decompose + decide + synth).
기존 7 multi-hop test 의 `ScriptedRetriever` 에 *probe-pass entry*
prepend + `retriever_handle.calls()` expectation +1. test 2 / test 4
처럼 entry 두 개였던 곳도 prepend (3 entries).
`multi_hop_refuse_no_chunks_preserves_hops_trace` /
`multi_hop_refuse_score_gate_preserves_hops_trace` 의 의미 좁힘 — 이제
*decompose-driven* refusal (probe pass 후 sub-query retrieve 가 empty
또는 below-gate) 만 검증. *probe-driven* refusal 은 hops=None
(decompose 안 함) — 신규 test 가 그 path 핀.
## 검증
- `cargo test -p kebab-rag -j 1` — 10 multi-hop (7 갱신 + 3 신규) + 19
pipeline + 31 unit + 3 prompt_template + 3 streaming 모두 통과. 회귀
없음.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
- 단일 crate 직렬 build (16 GB RAM 제약).
## 변경 없음
- Wire schema — `Answer.hops` shape 동일, `refusal_reason` enum 동일.
- 다른 도그푸딩 발견 (synthesize citation 일관성, latency, binary path
confusion) — v0.18.1 또는 별 PR 의 책임. HOTFIXES 의 "다른 도그푸딩
발견" 절에 명시.
## 다음
PR-7 머지 후:
1. Workspace `Cargo.toml` version 0.17.2 → 0.18.0 (minor bump).
2. HANDOFF.md / INDEX.md 갱신 + frozen design §3.8 multi-hop sub-section.
3. `gitea-release v0.18.0 --auto-notes`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test `spawn_snapshot_multi_hop_into_askopts` →
`ask_state_multi_hop_field_default_false_and_round_trips` 로 rename.
이전 이름은 spawn 동작 검증을 약속했으나 본문은 단순 field
default + setter round-trip 만 검증 — name 과 실제 의도의 mismatch.
새 이름이 실제 검증 (field shape pin) 과 정확히 일치.
doc string 도 spawn 동작은 별 path (live dogfood) 로 검증된다고
명확히 표기 — test 의 책임 범위가 무엇인지 reader 가 즉시 파악.
검증
- `cargo test -p kebab-tui -j 1 --test ask` — 42 test (6 multi-hop
포함) 모두 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fb-41 multi-hop RAG 의 **마지막 component PR** (PR-5 머지 직후). TUI Ask
패널의 user-facing surface — F2 toggle, multi-hop badge, status panel
의 hop count summary, cheatsheet 안내. v0.18.0 cut 준비.
설계: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
계획: docs/superpowers/plans/2026-05-25-p9-fb-41-multi-hop-rag.md (PR-6 단락)
## TUI surface
- `crates/kebab-tui/src/app.rs`:
- `AskState.multi_hop: bool` field + Default false. 사용자 토글
상태를 인-패널 보존, 대화 history 와 직교 — F2 flipping mid-
conversation 도 turns 보존 (다음 turn 만 다른 pipeline 으로 route).
- `crates/kebab-tui/src/ask.rs`:
- `handle_key_ask` 에 `(KeyCode::F(2), _) → s.multi_hop = !s.multi_hop`.
Mode-agnostic (physical function key — Normal/Insert 양쪽 작동, typing
ambiguity 없음). Briefing 의 candidate (F2 vs Ctrl-T) 중 F2 채택 —
Ctrl-M 은 Enter 와 collision 이미 명시, F2 가 cleanest.
- `spawn_ask_worker` 의 `AskOpts.multi_hop` 가 spawn 시점에 토글값
snapshot. 이후 F2 flip 은 다음 Enter 부터 적용 (in-flight turn 무영향).
- `render_input` 의 input pane title 에 `F2=multi-hop` binding 안내
추가 + prompt row 에 `multi-hop` badge (Success 녹색, toggled-on 일
때만). 사용자가 어떤 pipeline 으로 다음 query 를 보낼지 항상 가시.
- `render_status` 의 status panel 에 `multi-hop: N hops` line 추가
(last_answer.hops 가 Some 일 때만). forced_stop 발생 시
`forced_stop=K` suffix — depth/pool cap tuning 단서.
- `crates/kebab-tui/src/cheatsheet.rs`:
- Ask section 에 `F2 toggle multi-hop pipeline` entry 추가.
## 변경 없음 (의도된 deferral)
- `InspectTarget::Hop(turn_index)` variant — plan 의 PR-6 stretch goal.
per-iter hop trace detail 을 Inspect 패널에 노출하는 기능은 별 PR
(PR-6b 또는 v0.18 dogfood follow-up). PR-6 의 핵심 가치
(사용자가 multi-hop pipeline 을 토글하고 결과의 hop count 를 본다)
는 status panel 의 한 줄 summary 로 100% cover. Inspect 진입은
multi-hop 사용자가 *드물게* 필요한 surface — v0.18 cut 부담 회피.
- prompt_template_version (`rag-multi-hop-v1`) — 그대로.
- MCP / CLI surface — PR-4 / PR-5 의 책임.
## Tests (`tests/ask.rs` 신규 6 multi-hop pins)
- `f2_toggles_multi_hop_flag_from_insert_mode`: Insert 에서 F2 toggle
(fresh_app default mode).
- `f2_toggles_multi_hop_flag_from_normal_mode`: Normal 에서도 동일
— mode-agnostic 회귀 핀.
- `input_pane_shows_multi_hop_badge_when_toggled_on`: 토글 on 시
prompt row 에 `multi-hop` 등장 + title 의 `F2=multi-hop` binding
hint 등장.
- `input_pane_omits_multi_hop_badge_when_toggled_off`: 토글 off 시
prompt row 의 badge 부재 (title hint 는 유지 — 사용자 discoverability).
- `status_panel_summarizes_hops_when_answer_has_trace`: 3-hop trace
(Decompose + Decide + Synthesize) → `multi-hop: 3 hops` line.
- `status_panel_omits_hops_summary_for_single_pass`: hops=None → 본문
에 summary line 부재 (title binding hint 만).
- `spawn_snapshot_multi_hop_into_askopts`: AskState.multi_hop 의
field shape 회귀 핀 (default false / settable / round-trip).
## 검증
- `cargo test -p kebab-tui -j 1` — 신규 6 multi-hop + 기존 ask /
search / library / mode / cheatsheet / inspect / status_bar 모두
통과 (42 ask test + 10 mode + 기타). 회귀 없음.
- `cargo clippy -p kebab-tui --all-targets -j 1 -- -D warnings` clean.
- 단일 crate 직렬 build (16 GB RAM 제약).
## v0.18.0 cut (다음 단계)
- Workspace `Cargo.toml` version 0.17.2 → 0.18.0 (minor — surface
확장 + new prompt_template_version `rag-multi-hop-v1`).
- HANDOFF.md / HOTFIXES.md / INDEX.md 갱신 (fb-41 entry 정리).
- `gitea-release v0.18.0 --auto-notes`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`ask_tool_routes_multi_hop_true_to_decompose_first` 의 error code
검증을 더 견고하게 — `model_unreachable | timeout` 둘 다 accept.
환경 차이 (즉시 ECONNREFUSED vs connect timeout) 가 다른 wire code
로 분류돼도 dispatch divergence 자체 (schema_version=error.v1 +
isError=true vs single-pass 의 answer.v1 grounded=false) 는 동일하게
검증.
검증
- `cargo test -p kebab-mcp -j 1 --test tools_call_ask_multi_hop` 2 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`answer.schema.json` 의 `refusal_reason` description 의 PR 번호 정정:
`multi_hop_decompose_failed` 도입 시점 = PR-2 (#167, RefusalReason variant
+ ask_multi_hop decompose-failure 분기). PR-3a (#168) 는 `Answer.hops`
field + RagCfg knob 만 — refusal variant 와 무관.
검증
- `cargo test -p kebab-cli -j 1 --test wire_ask_multi_hop` 4 모두 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test 7 의 `i32_below_gate_chunk` helper rename → `seed_low_score_chunk` +
반환 shape 을 `(chunk_id, doc_id)` tuple 로 확장. `i32` prefix 가 Rust
integer 타입과 충돌하던 가독성 문제 해소 + 호출자가 `id32("d_low")` 를
재계산하지 않도록 id 페어를 single source of truth 로 통합.
검증
- `cargo test -p kebab-rag -j 1 --test multi_hop` — 7 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(A) ScriptedLm doc 의 `Arc<Vec<String>>` 표기 → 실제 구현 (`Vec<String>` +
`AtomicUsize`, 외부에서 `Arc::new(ScriptedLm::new(...))` 로 wrap)
반영.
(B) ScriptedLm::new doc 의 미존재 `with_*` builder 언급 제거.
(C) refuse path 의 hops 보존 회귀 핀 2 건 추가 (`tests/multi_hop.rs`):
- `multi_hop_refuse_no_chunks_preserves_hops_trace`: empty pool →
`refuse_no_chunks(Some(hops))` → Answer.hops = Some([Decompose,
Decide]).
- `multi_hop_refuse_score_gate_preserves_hops_trace`: top score 0.10
< 0.30 gate → `refuse_score_gate(Some(hops))` → 같은 shape.
refuse_* widening + ask_multi_hop 의 forwarding wiring 이 reverting
되면 두 test 가 회귀 잡음.
(D) test 5 의 redundant `assert_ne!(.., Some(MultiHopDecomposeFailed))`
제거 — `assert_eq!(.., None)` 이미 함의. 메시지에 의도 통합.
검증
- `cargo test -p kebab-rag -j 1 --test multi_hop` — 7 (5+2) 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR-3b 의 분할 첫 PR. ask_multi_hop 의 fixed depth=2 → dynamic N-hop.
ScriptedLm helper + 5+ integration tests (happy-path 통합 검증) 는
PR-3b-ii 분리. 본 PR 의 회귀 핀 = 기존 PR-2 의 2 integration test
통과 (decompose garbage refusal + multi_hop=false single-pass keep).
- `RagPipeline::multi_hop_decompose` 시그니처 변경 — `Result<
(Option<Vec<String>>, u32)>` (parsed result + LLM call latency_ms).
caller (`ask_multi_hop`) 가 hop trace 의 `llm_call_ms` stamp.
- `RagPipeline::multi_hop_decide` helper 신규. decide LLM call →
`parse_decompose_response` 으로 `Option<Vec<String>>` 반환. None
또는 empty array 가 stop signal (refusal 아닌 graceful degrade).
- `MULTI_HOP_DECIDE_SYSTEM_PROMPT` const 신규.
- `MULTI_HOP_DECOMPOSE_USER_TEMPLATE` const 제거 + `format!` named
arg 사용 (PR-2 회차 1 carry-over fix). compile-time substitution
check — 사용자 query 안에 `{max_sub_queries}` literal 있어도
mis-replace 회피.
- `ask_multi_hop` 의 §1 (Decompose) + §2 (Retrieve) 영역을 dynamic
loop 으로 재작성:
- iter 0 = decompose, HopRecord 추가 (kind=Decompose).
- iter 1..=max_depth = retrieve current_sub_queries → pool dedup
→ decide LLM call (forced_stop / pool_cap_hit 시 skip).
HopRecord 추가 (kind=Decide, sub_queries=new_sub_queries,
context_chunks_added, forced_stop, llm_call_ms).
- `max_pool_chunks` 도달 시 `pool_cap_hit = true` → 그 iter 의
HopRecord 가 `forced_stop = true` + decide LLM call skip.
- depth 도달 (`iter >= max_depth`) 시 동일하게 forced_stop.
- decide parse failure 또는 empty array → loop break (early
synthesize, NOT refusal — spec §9 graceful degrade).
- §6 (Generate) 시작 시 `synthesize_started: Instant::now()` 별
stamp → §8 Build Answer 직전 `HopRecord { kind=Synthesize,
llm_call_ms = synth_ms }` 추가. happy path 의 Answer literal
`hops: Some(hops)` 채움 (`hops: None` → `Some(...)` 변경).
- doc comment 갱신: "PR-2 scope (fixed depth=2)" → "PR-3b-i
scope (dynamic N-hop)". refusal path 의 hops trace 손실 caveat
명시 (PR-3b-ii / follow-up 에서 helper signature 확장 시 해결).
기존 회귀 핀 (PR-2 의 2 integration test):
- `ask_multi_hop_dispatches_and_decompose_garbage_refuses`:
decompose garbage → RefusalReason::MultiHopDecomposeFailed +
정확히 1 LLM call. PR-3b-i 의 시그니처 변경 후도 통과.
- `ask_with_multi_hop_false_keeps_single_pass_path`: 영향 없음.
56 unit + integration test 모두 통과 (kebab-rag).
Wire 영향: `Answer.hops` 가 multi-hop happy path 에서 emit. JSON
Schema additionalProperties default `true` 라 wire breaking 아님
(PR-3a 의 review 확인). schema.json 명시 갱신은 별 PR
(PR-3b-ii 또는 PR-4 의 schema sweep).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- `parse_decompose_response_drops_partial_empty_keeps_valid` 신규
회귀 핀 — `["", "valid q", " "]` → `["valid q"]` (trim+filter
chain 동작 pin).
- `multi_hop_decompose` 의 `stop: Vec::new()` 옆 doc comment 추가 —
의도 명시 (instruction-following 모델 기대 + prose 추가 시
MultiHopDecomposeFailed refusal 가 policy). 회차 1 question 의
답변.
- plan 의 PR-3 implementation order 에 회차 1 carry-over 추가:
1) ask + ask_multi_hop 의 §4-§9 mirror → 공통 helper 추출,
2) decompose template 의 substitution corner case → format!
named arg 으로 교체.
회차 1 의 다른 suggestion (mirror refactor, substitution corner
case, history block helper) 는 PR-3 합리적 timing 으로 plan 에
명시 — 회차 2 reply 에 정리.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR-2 of fb-41 multi-hop RAG. Decompose + retrieve + synthesize 3-stage
pipeline가 `opts.multi_hop=true` 일 때 dispatch. Dynamic decide loop
는 PR-3.
- `AskOpts.multi_hop: bool` 필드 추가 + `impl Default for AskOpts`
도입 (HOTFIXES 2026-05-07 의 known limitation 해소). 9 explicit
init site 모두 `multi_hop: false` 추가 — Default 도입으로 향후
`..Default::default()` 점진 migrate 가능.
- `RagPipeline::ask` 의 entry 에 dispatcher 한 줄
(`if opts.multi_hop { return self.ask_multi_hop(...) }`).
- `RagPipeline::ask_multi_hop` 신규 method. 1) decompose LLM call
→ JSON array of strings parse, 2) 각 sub-query 로 retrieve +
chunk_id dedup pool, 3) score gate / no-chunks 가드, 4)
pack_context (single-pass 와 helper 공유), 5) synthesize LLM
call w/ MULTI_HOP_SYNTHESIZE_SYSTEM_PROMPT, 6) citation extract
+ Answer build. `prompt_template_version` = "rag-multi-hop-v1"
로 stamp — eval `compare` 가 single-pass vs multi-hop 분리.
- Prompt const 신규: MULTI_HOP_DECOMPOSE_SYSTEM_PROMPT +
MULTI_HOP_DECOMPOSE_USER_TEMPLATE + MULTI_HOP_SYNTHESIZE_SYSTEM_PROMPT
+ PROMPT_TEMPLATE_VERSION_MULTI_HOP + MULTI_HOP_MAX_SUB_QUERIES_DEFAULT.
- `kebab_core::RefusalReason::MultiHopDecomposeFailed` variant 신규.
Cascade: kebab-store-sqlite `refusal_reason_label` + kebab-tui `ask
refusal render` exhaustive match 갱신.
- `parse_decompose_response` + `strip_markdown_json_fence` helper —
markdown code fence (```json / ```) strip + JSON array of strings
parse + trim + drop empty + cap at MULTI_HOP_MAX_SUB_QUERIES_DEFAULT.
None 반환 시 caller 가 `MultiHopDecomposeFailed` refusal.
Tests (55 passing total, 8 신규):
- 6 unit (parse_decompose_response 의 bare array / fence variants /
garbage / cap / trim 회귀 핀).
- 2 integration: `ask_multi_hop_dispatches_and_decompose_garbage_refuses`
(decompose garbage → MultiHopDecomposeFailed + 정확히 1 LLM call) +
`ask_with_multi_hop_false_keeps_single_pass_path` (회귀 핀, 기존
caller 자동 backwards-compat).
Happy-path multi-hop (decompose 성공 → synthesize) 의 integration
test 는 ScriptedLm helper 가 PR-3 의 decide loop 와 함께 도입될
때 같이 추가. 현 `MockLanguageModel` 는 canned single response 라
2-LLM-call sequence 핀 불가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>