feat(rag): fb-41 PR-3b-ii ScriptedLm + multi-hop tests + refusal hop trace #170

Merged
altair823 merged 3 commits from feat/fb-41-pr-3b-ii-scripted-lm-tests into main 2026-05-25 08:25:44 +00:00
Owner

요약

fb-41 multi-hop RAG 의 PR-3b-ii (PR-3b 의 분할 두 번째 PR, test 인프라 + 회차 1 carry-over). PR-3b-i 의 dynamic decide loop 위에서 ScriptedLm test helper + 5 multi-hop integration tests + refuse_* helper 의 hops trace 보존 + 회차 1 carry-over (HopRecord doc / const reconcile / decide preview doc).

설계: 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-3b 단락)

Test 인프라

  • ScriptedLm (tests/common/mod.rs) — Vec<&str> 받아 call sequence 순서대로 emit 하는 LanguageModel impl. multi-hop 의 decompose / decide×N / synthesize 각 call 에 다른 response 주입 가능. Send + Sync. Exhaustion 시 call index 명시한 panic.
  • ScriptedRetriever (tests/common/mod.rs) — Vec<Vec<SearchHit>> 받아 call 순서대로 hits 반환. pool dedup / iter 별 다른 retrieve 결과 시뮬레이션. exhaustion 시 empty Vec (pipeline 의 정상 path 와 정합).

5 multi-hop integration tests (tests/multi_hop.rs 신규)

  • decide_stop_triggers_synthesize — decide [] → 즉시 synthesize, hops=[Decompose, Decide(stop), Synthesize].
  • decide_continue_adds_more_chunks — decide [\"q2\"] → iter 2 retrieve + pool 확장, hops 길이 4.
  • max_depth_force_stopsmulti_hop_max_depth=1 → depth cap 발동, decide LLM call skip + forced_stop=true.
  • pool_chunks_dedup_by_chunk_id — 두 sub-query 가 같은 chunk_id → dedup 후 pool 1 개, context_chunks_added=1.
  • decide_parse_failure_falls_through_to_synthesize — decide non-JSON garbage → graceful synthesize (NOT refusal, spec §9).

refuse_* helper 의 hops trace 보존

refuse_no_chunks / refuse_score_gate 시그니처에 hops: Option<Vec<HopRecord>> 추가. ask_multi_hop 의 score-gate / no-chunks refusal 시 누적된 hops 그대로 Answer.hops 에 보존 — `--multi-hop` 사용자가 refusal 경로에서도 어느 decompose / decide 신호까지 발생했는지 볼 수 있음. single-pass ask 는 None 전달, wire 변동 없음 (`skip_serializing_if = None`).

회차 1 carry-over (PR #168 + #169 의 review 항목)

  • HopRecord.sub_queries doc per-kind 명시 (kebab-core) — Decompose=initial sub_queries, Decide=next-iter or empty=stop, Synthesize=always empty.
  • HopRecord.llm_call_ms ambiguity doc — `0` 의 의미 (no call vs 0ms call) 명시. forced_stop / parse-degraded decide / empty-pool 경로가 `0` 을 emit.
  • MULTI_HOP_MAX_SUB_QUERIES_DEFAULT → _HARD_CAP rename (kebab-rag) — const 의 의도 명확화. config knob `multi_hop_max_sub_queries_per_iter` (5, prompt-side soft hint) 와 const (10, parse-side hard ceiling) 의 두 layer 분리 + doc 동기화 (kebab-config 의 field doc 도 갱신).
  • parse_decompose_response post-condition doc — Some(non_empty) 보장. defensive guard 단순화 (`Some(qs) if !qs.is_empty()` → `decide_result.unwrap_or_default()`).
  • decide preview snippet-only path doc — 모든 pool entry 의 snippet concat (full chunk text 안 fetch). `max_pool_chunks` 가 implicit cap. pack_context 안 거치는 의도 (decide 는 gist 만 판단, 전체 text 는 terminal synthesize 용) 명시.

변경 없음

  • Wire schema (`answer.v1` / `error.v1`) — refusal 의 hops 보존은 additive (`skip_serializing_if = None` 유지).
  • RagCfg knob 값 — default 동일.
  • prompt_template_version — `rag-multi-hop-v1` 그대로.
  • 외부 CLI / MCP / TUI surface — PR-4 / PR-5 / PR-6 에서.

검증

  • `cargo test -p kebab-rag -j 1` — 31 unit + 19 pipeline + 5 multi_hop + 3 prompt_template + 3 streaming 모두 통과.
  • `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
  • 단일 crate 직렬 build (16 GB RAM 제약).

시험 항목 (Test Plan)

  • decide stop signal → synthesize transition pin
  • decide continue → iter 2 retrieve + pool grow pin
  • depth cap → forced_stop + decide skip pin
  • pool dedup by chunk_id pin
  • decide parse fail → graceful synthesize (NOT refusal) pin
  • ScriptedLm exhaustion panic 메시지 (테스트가 의도치 않은 추가 call 시 명확한 실패)
  • refuse_* helper signature widening 후 single-pass ask 회귀 없음 (기존 pipeline.rs tests 19 건 통과)

다음 PR

  • PR-4: CLI `--multi-hop` flag + answer.v1 JSON Schema (hops 명시) + error.v1 code `multi_hop_decompose_failed`.
  • PR-5: MCP ask tool `multi_hop: bool` + SKILL.md.
  • PR-6: TUI Ask 패널 multi-hop toggle + hop trace render.
  • v0.18.0 cut (PR-6 머지 후).

Assisted-by: Claude Code

## 요약 fb-41 multi-hop RAG 의 **PR-3b-ii** (PR-3b 의 분할 두 번째 PR, test 인프라 + 회차 1 carry-over). PR-3b-i 의 dynamic decide loop 위에서 `ScriptedLm` test helper + 5 multi-hop integration tests + refuse_* helper 의 hops trace 보존 + 회차 1 carry-over (HopRecord doc / const reconcile / decide preview doc). 설계: 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-3b 단락) ## Test 인프라 - `ScriptedLm` (`tests/common/mod.rs`) — `Vec<&str>` 받아 call sequence 순서대로 emit 하는 LanguageModel impl. multi-hop 의 decompose / decide×N / synthesize 각 call 에 다른 response 주입 가능. Send + Sync. Exhaustion 시 call index 명시한 panic. - `ScriptedRetriever` (`tests/common/mod.rs`) — `Vec<Vec<SearchHit>>` 받아 call 순서대로 hits 반환. pool dedup / iter 별 다른 retrieve 결과 시뮬레이션. exhaustion 시 empty Vec (pipeline 의 정상 path 와 정합). ## 5 multi-hop integration tests (`tests/multi_hop.rs` 신규) - `decide_stop_triggers_synthesize` — decide `[]` → 즉시 synthesize, hops=[Decompose, Decide(stop), Synthesize]. - `decide_continue_adds_more_chunks` — decide `[\"q2\"]` → iter 2 retrieve + pool 확장, hops 길이 4. - `max_depth_force_stops` — `multi_hop_max_depth=1` → depth cap 발동, decide LLM call skip + forced_stop=true. - `pool_chunks_dedup_by_chunk_id` — 두 sub-query 가 같은 chunk_id → dedup 후 pool 1 개, `context_chunks_added`=1. - `decide_parse_failure_falls_through_to_synthesize` — decide non-JSON garbage → graceful synthesize (NOT refusal, spec §9). ## refuse_* helper 의 hops trace 보존 `refuse_no_chunks` / `refuse_score_gate` 시그니처에 `hops: Option<Vec<HopRecord>>` 추가. ask_multi_hop 의 score-gate / no-chunks refusal 시 누적된 hops 그대로 Answer.hops 에 보존 — \`--multi-hop\` 사용자가 refusal 경로에서도 어느 decompose / decide 신호까지 발생했는지 볼 수 있음. single-pass ask 는 None 전달, wire 변동 없음 (\`skip_serializing_if = None\`). ## 회차 1 carry-over (PR #168 + #169 의 review 항목) - **HopRecord.sub_queries doc per-kind 명시** (kebab-core) — Decompose=initial sub_queries, Decide=next-iter or empty=stop, Synthesize=always empty. - **HopRecord.llm_call_ms ambiguity doc** — \`0\` 의 의미 (no call vs 0ms call) 명시. forced_stop / parse-degraded decide / empty-pool 경로가 \`0\` 을 emit. - **MULTI_HOP_MAX_SUB_QUERIES_DEFAULT → _HARD_CAP rename** (kebab-rag) — const 의 의도 명확화. config knob \`multi_hop_max_sub_queries_per_iter\` (5, prompt-side soft hint) 와 const (10, parse-side hard ceiling) 의 두 layer 분리 + doc 동기화 (kebab-config 의 field doc 도 갱신). - **parse_decompose_response post-condition doc** — Some(non_empty) 보장. defensive guard 단순화 (\`Some(qs) if !qs.is_empty()\` → \`decide_result.unwrap_or_default()\`). - **decide preview snippet-only path doc** — 모든 pool entry 의 snippet concat (full chunk text 안 fetch). \`max_pool_chunks\` 가 implicit cap. pack_context 안 거치는 의도 (decide 는 gist 만 판단, 전체 text 는 terminal synthesize 용) 명시. ## 변경 없음 - Wire schema (\`answer.v1\` / \`error.v1\`) — refusal 의 hops 보존은 additive (\`skip_serializing_if = None\` 유지). - RagCfg knob 값 — default 동일. - prompt_template_version — \`rag-multi-hop-v1\` 그대로. - 외부 CLI / MCP / TUI surface — PR-4 / PR-5 / PR-6 에서. ## 검증 - \`cargo test -p kebab-rag -j 1\` — 31 unit + 19 pipeline + **5 multi_hop** + 3 prompt_template + 3 streaming 모두 통과. - \`cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings\` clean. - 단일 crate 직렬 build (16 GB RAM 제약). ## 시험 항목 (Test Plan) - [x] decide stop signal → synthesize transition pin - [x] decide continue → iter 2 retrieve + pool grow pin - [x] depth cap → forced_stop + decide skip pin - [x] pool dedup by chunk_id pin - [x] decide parse fail → graceful synthesize (NOT refusal) pin - [x] ScriptedLm exhaustion panic 메시지 (테스트가 의도치 않은 추가 call 시 명확한 실패) - [x] refuse_* helper signature widening 후 single-pass ask 회귀 없음 (기존 pipeline.rs tests 19 건 통과) ## 다음 PR - PR-4: CLI \`--multi-hop\` flag + answer.v1 JSON Schema (hops 명시) + error.v1 code \`multi_hop_decompose_failed\`. - PR-5: MCP ask tool \`multi_hop: bool\` + SKILL.md. - PR-6: TUI Ask 패널 multi-hop toggle + hop trace render. - v0.18.0 cut (PR-6 머지 후). Assisted-by: Claude Code
altair823 added 1 commit 2026-05-25 08:18:44 +00:00
PR-3b 의 분할 두 번째 PR — PR-3b-i 의 dynamic decide loop 위에서:

1. **ScriptedLm + ScriptedRetriever helper** (kebab-rag tests/common/mod.rs)
   per-call 다른 response 반환. decompose / decide×N / synthesize 의 각
   LLM call 을 구분하는 다단계 multi-hop 시나리오를 mock-only 로 exercise
   가능. `Vec<&str>` / `Vec<Vec<SearchHit>>` 받아 call sequence 순서대로
   emit. Send + Sync.

2. **5 multi-hop integration tests** (kebab-rag tests/multi_hop.rs 신규)
   - decide_stop_triggers_synthesize: decide [] → 즉시 synthesize
   - decide_continue_adds_more_chunks: decide ["q2"] → iter 2 retrieve + pool 확장
   - max_depth_force_stops: depth cap → forced_stop + decide LLM call skip
   - pool_chunks_dedup_by_chunk_id: 같은 chunk_id 두 sub-query 에서 1 회
   - decide_parse_failure_falls_through_to_synthesize: parse fail = graceful
     synthesize (refusal 아님, spec §9)

3. **refuse_* helper hops trace 보존** (회차 1 carry-over)
   refuse_no_chunks / refuse_score_gate 시그니처에 `hops:
   Option<Vec<HopRecord>>` 인자 추가. ask_multi_hop 의 score-gate /
   no-chunks refusal 시 누적된 hops 그대로 Answer.hops 에 보존.
   single-pass ask 는 None 전달 — wire 변동 없음 (skip_serializing_if).

4. **HopRecord doc 보강** (회차 1 carry-over)
   sub_queries 의 per-kind 의미 명시 (Decompose=initial / Decide=next-iter
   or empty=stop / Synthesize=always empty). llm_call_ms=0 의 ambiguity
   (no call vs 0ms call) doc 명시.

5. **MULTI_HOP_MAX_SUB_QUERIES_DEFAULT → _HARD_CAP rename** (회차 1 carry-over)
   const 의 의도 명확화 — config knob `multi_hop_max_sub_queries_per_iter`
   (5, prompt-side soft hint) 와 const (10, parse-side hard ceiling)
   분리. 두 layer 의 책임 doc 동기화. test 도 rename.

6. **decide guard 단순화 + preview budget doc** (회차 1 carry-over)
   parse_decompose_response 의 post-condition (Some=non-empty 보장)
   doc 명시. defensive `Some(qs) if !qs.is_empty()` →
   `decide_result.unwrap_or_default()` 단순화. decide preview 의
   snippet-only path (full chunk text 안 fetch) 의도 doc.

검증
- `cargo test -p kebab-rag -j 1` — 31 unit + 19 pipeline + 5 multi_hop
  + 3 prompt_template + 3 streaming 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.

Spec / plan
- design: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
- plan: docs/superpowers/plans/2026-05-25-p9-fb-41-multi-hop-rag.md (PR-3b 단락)

다음 단계 = PR-4 (CLI --multi-hop + wire schema + error_wire).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-25 08:20:52 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — 4 actionable 항목.

작은 doc / nit 두 개 (A, B): ScriptedLm 의 module doc 두 군데가 실제 구현과 어긋난다. Arc<Vec<String>> 표기 + 존재하지 않는 with_* builder 언급. 두 줄 정정.

가장 무거운 한 항목 (C): PR 의 두 핵심 변경 중 하나 — refuse_* helper 의 hops widening + ask_multi_hop refusal path 의 hops 보존 wiring — 가 5 integration test 중 어느 것에도 직접 핀되지 않는다. happy-path 만 검증하니 누군가 widening 부분을 reverting 해도 5 test 모두 통과. 짝 test 1~2 개로 NoChunks / ScoreGate refusal 시 Answer.hops 가 None 이 아니라 partial trace 임을 회귀 핀.

nit (D): test 5 의 assert_eq!(.., None) + assert_ne!(.., Some(MultiHopDecomposeFailed)) — 두 번째가 첫 번째에 trivial 하게 함의됨. 한 assertion 으로 합치고 메시지에 의도 명시.

회차 2 에 (A) (B) (C) (D) 모두 반영.

설계·계획 정합 / 코드 흐름 / clippy 측면은 깨끗. test 1~4 의 시나리오 / mock 패턴 / hop trace 검증 / dedup 검증은 잘 짜여 있고, ScriptedLm + ScriptedRetriever 의 exhaustion policy 비대칭 (panic vs empty Vec) 도 doc 으로 rationale 명시 — 적절.

회차 1 — 4 actionable 항목. **작은 doc / nit 두 개 (A, B)**: `ScriptedLm` 의 module doc 두 군데가 실제 구현과 어긋난다. `Arc<Vec<String>>` 표기 + 존재하지 않는 `with_*` builder 언급. 두 줄 정정. **가장 무거운 한 항목 (C)**: PR 의 두 핵심 변경 중 하나 — `refuse_*` helper 의 hops widening + ask_multi_hop refusal path 의 hops 보존 wiring — 가 5 integration test 중 어느 것에도 직접 핀되지 않는다. happy-path 만 검증하니 누군가 widening 부분을 reverting 해도 5 test 모두 통과. 짝 test 1~2 개로 NoChunks / ScoreGate refusal 시 `Answer.hops` 가 None 이 아니라 partial trace 임을 회귀 핀. **nit (D)**: test 5 의 `assert_eq!(.., None)` + `assert_ne!(.., Some(MultiHopDecomposeFailed))` — 두 번째가 첫 번째에 trivial 하게 함의됨. 한 assertion 으로 합치고 메시지에 의도 명시. 회차 2 에 (A) (B) (C) (D) 모두 반영. 설계·계획 정합 / 코드 흐름 / clippy 측면은 깨끗. test 1~4 의 시나리오 / mock 패턴 / hop trace 검증 / dedup 검증은 잘 짜여 있고, ScriptedLm + ScriptedRetriever 의 exhaustion policy 비대칭 (panic vs empty Vec) 도 doc 으로 rationale 명시 — 적절.

doc-code mismatch: Internally \Arc<Vec> + AtomicUsize`라고 적혀 있지만 실제 필드는responses: Vec+next: AtomicUsizeArc가 안에 있는 게 아니라 *밖에서* 사용자가Arc::new(ScriptedLm::new(...))로 감싼다. Send + Sync 가 되는 이유도Vec이 Send + Sync 라서지Arc가 들어 있어서가 아니다. doc 한 줄을Internally `Vec` (immutable after construction) + `AtomicUsize` so the type is `Send + Sync` ...` 로 정정.

**doc-code mismatch**: `Internally \`Arc<Vec<String>> + AtomicUsize\`` 라고 적혀 있지만 실제 필드는 `responses: Vec<String>` + `next: AtomicUsize` — `Arc` 가 안에 있는 게 아니라 *밖에서* 사용자가 `Arc::new(ScriptedLm::new(...))` 로 감싼다. Send + Sync 가 되는 이유도 `Vec<String>` 이 Send + Sync 라서지 `Arc` 가 들어 있어서가 아니다. doc 한 줄을 `Internally \`Vec<String>\` (immutable after construction) + \`AtomicUsize\` so the type is \`Send + Sync\` ...` 로 정정.

미존재 API 언급: Call \with_*` builders if a test needs to override the defaults라고 했지만 코드에with_model_id/with_provider` 같은 builder 가 없다. 둘 중 하나로:

  • (가벼움) 이 한 줄 제거 — 현재 모든 test 는 default 만 써도 충분.
  • (확장 시) 실제 builder 추가 후 doc 유지.

현 PR 의 범위 측면에서는 line 제거가 단순함. 향후 필요해지면 그때 builder + doc 같이 추가.

**미존재 API 언급**: `Call \`with_*\` builders if a test needs to override the defaults` 라고 했지만 코드에 `with_model_id` / `with_provider` 같은 builder 가 없다. 둘 중 하나로: - (가벼움) 이 한 줄 제거 — 현재 모든 test 는 default 만 써도 충분. - (확장 시) 실제 builder 추가 후 doc 유지. 현 PR 의 범위 측면에서는 line 제거가 단순함. 향후 필요해지면 그때 builder + doc 같이 추가.
@@ -0,0 +172,4 @@
assert_eq!(hops[3].kind, HopKind::Synthesize);
}
// ── 3. max_depth=1 → forced_stop, decide LLM call skipped ─────────────────

PR 의 핵심 변경에 대한 회귀 핀 누락: 이 PR 이 refuse_no_chunks / refuse_score_gate 시그니처를 widening 해서 hops: Option<Vec<HopRecord>> 받게 했고, ask_multi_hop 의 refusal 경로에서 누적된 hops 를 보존하도록 wiring 했다. 하지만 5 integration test 중 하나도 "multi-hop refusal 경로에서 Answer.hops 가 None 이 아니라 partial trace" 를 직접 검증하지 않는다.

구체 시나리오: ScriptedRetriever::new(vec![vec![]]) (iter 1 retrieve 가 0 hits) + decompose ok → pool 비어 있음 → refuse_no_chunks(Some(hops)) 분기. assert:

  • answer.refusal_reason == Some(NoChunks)
  • answer.hops is Some(non_empty) — 최소 Decompose + Decide(empty) 두 entry
  • hops[0].kind == Decompose

refuse_score_gate 쪽도 비슷한 케이스 하나 추가하면 양쪽 path 모두 핀. 회귀 위험 ↑ — 누군가 widening 을 reverting 해도 happy-path tests 5 건은 그대로 통과한다.

**PR 의 핵심 변경에 대한 회귀 핀 누락**: 이 PR 이 `refuse_no_chunks` / `refuse_score_gate` 시그니처를 widening 해서 `hops: Option<Vec<HopRecord>>` 받게 했고, ask_multi_hop 의 refusal 경로에서 누적된 hops 를 보존하도록 wiring 했다. 하지만 5 integration test 중 하나도 "multi-hop refusal 경로에서 Answer.hops 가 None 이 아니라 partial trace" 를 직접 검증하지 않는다. 구체 시나리오: `ScriptedRetriever::new(vec![vec![]])` (iter 1 retrieve 가 0 hits) + decompose ok → pool 비어 있음 → `refuse_no_chunks(Some(hops))` 분기. assert: - `answer.refusal_reason == Some(NoChunks)` - `answer.hops` is `Some(non_empty)` — 최소 Decompose + Decide(empty) 두 entry - `hops[0].kind == Decompose` refuse_score_gate 쪽도 비슷한 케이스 하나 추가하면 양쪽 path 모두 핀. 회귀 위험 ↑ — 누군가 widening 을 reverting 해도 happy-path tests 5 건은 그대로 통과한다.
@@ -0,0 +282,4 @@
assert_eq!(hops[0].sub_queries, vec!["q1", "q2"]);
assert_eq!(
hops[1].context_chunks_added, 1,
"dedup reduces 2 retrievals → 1 new pool entry"

redundant assertion (nit): assert_eq!(answer.refusal_reason, None) 다음에 바로 assert_ne!(answer.refusal_reason, Some(MultiHopDecomposeFailed)) 가 있는데, None != Some(_) 은 trivial 하게 참. 메시지의 의도 ("MultiHopDecomposeFailed is reserved for the initial decompose hop") 자체는 가치 있지만 assert 로는 정보 추가 안 함.

선택지:

  • assert_eq! 만 남기고 assert_ne! 제거 (메시지를 assert_eq! 의 msg 로 합치기 — "decide parse failure is graceful degrade, not refusal — MultiHopDecomposeFailed is reserved for the initial decompose hop").
  • 또는 assert_ne! 만 두고 assert_eq! 제거 — 의도는 "MultiHopDecomposeFailed 가 아니다" 가 핵심이므로.

둘 다 의도 동일. 한 assertion 으로 줄이고 메시지를 합치는 게 깔끔.

**redundant assertion (nit)**: `assert_eq!(answer.refusal_reason, None)` 다음에 바로 `assert_ne!(answer.refusal_reason, Some(MultiHopDecomposeFailed))` 가 있는데, `None != Some(_)` 은 trivial 하게 참. 메시지의 의도 ("MultiHopDecomposeFailed is reserved for the initial decompose hop") 자체는 가치 있지만 assert 로는 정보 추가 안 함. 선택지: - `assert_eq!` 만 남기고 `assert_ne!` 제거 (메시지를 `assert_eq!` 의 msg 로 합치기 — "decide parse failure is graceful degrade, not refusal — MultiHopDecomposeFailed is reserved for the initial decompose hop"). - 또는 `assert_ne!` 만 두고 `assert_eq!` 제거 — 의도는 "MultiHopDecomposeFailed 가 아니다" 가 핵심이므로. 둘 다 의도 동일. 한 assertion 으로 줄이고 메시지를 합치는 게 깔끔.
altair823 added 1 commit 2026-05-25 08:23:03 +00:00
(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>
claude-reviewer-01 requested changes 2026-05-25 08:24:02 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 (A) (B) (C) (D) 모두 반영. 작은 함수 이름 nit 1 건 남음.

반영 확인:

  • (A) ScriptedLm doc 의 Arc<Vec<String>> 표기 정정 ✓
  • (B) with_* builder 미존재 언급 제거 ✓
  • (C) refuse path hops 보존 회귀 핀 2 건 추가 — multi_hop_refuse_no_chunks_preserves_hops_trace + multi_hop_refuse_score_gate_preserves_hops_trace. NoChunks (decide 도 skip) 와 ScoreGate (decide ran) 두 케이스가 다른 hop trace shape 까지 핀해서 강도 ↑ ✓
  • (D) test 5 의 redundant assert_ne! 제거 + 메시지 통합 ✓

회차 2 nit: test 7 의 i32_below_gate_chunk helper 함수 이름이 i32 Rust 타입과 충돌해 첫 인상이 헷갈린다. inline 코멘트에 rename + 반환 shape 정리 제안.

회차 3 에서 이 한 가지만 반영하면 APPROVE.

회차 2 — 회차 1 의 (A) (B) (C) (D) 모두 반영. 작은 함수 이름 nit 1 건 남음. **반영 확인**: - (A) ScriptedLm doc 의 `Arc<Vec<String>>` 표기 정정 ✓ - (B) `with_*` builder 미존재 언급 제거 ✓ - (C) refuse path hops 보존 회귀 핀 2 건 추가 — `multi_hop_refuse_no_chunks_preserves_hops_trace` + `multi_hop_refuse_score_gate_preserves_hops_trace`. NoChunks (decide 도 skip) 와 ScoreGate (decide ran) 두 케이스가 다른 hop trace shape 까지 핀해서 강도 ↑ ✓ - (D) test 5 의 redundant `assert_ne!` 제거 + 메시지 통합 ✓ **회차 2 nit**: test 7 의 `i32_below_gate_chunk` helper 함수 이름이 `i32` Rust 타입과 충돌해 첫 인상이 헷갈린다. inline 코멘트에 rename + 반환 shape 정리 제안. 회차 3 에서 이 한 가지만 반영하면 APPROVE.
@@ -0,0 +450,4 @@
fn i32_below_gate_chunk(env: &RagEnv) -> String {
let cid = id32("c_low");
let did = id32("d_low");
env.seed_chunk(&cid, &did, "notes/low.md", "low score text", &["Low"]);

함수 이름 nit (actionable): i32_below_gate_chunki32_ prefix 는 Rust integer 타입 i32 와 충돌해서 첫인상이 헷갈린다 (의도는 id32 헬퍼를 부르는 wrapper 라는 뜻이겠지만 그 정보도 호출자에게 가치 없음). 더 의도가 드러나는 이름:

  • seed_below_gate_chunk (action + condition)
  • seed_low_score_chunk (action + 성격)

동시에 함수가 cid 만 반환하면 호출자가 id32("d_low")다시 만들어야 하는데 (line 410 의 mk_hit(1, &cid, &id32("d_low"), ...)), 이 doc_id 도 함수 안의 seed 와 같은 id32 결과라 dual source-of-truth. (chunk_id, doc_id) tuple 또는 바로 SearchHit 까지 만들어 반환하면 호출자 본문이 더 짧아지고 헬퍼-호출자 사이의 id 동기화 깨질 위험도 없어진다.

**함수 이름 nit (actionable)**: `i32_below_gate_chunk` 의 `i32_` prefix 는 Rust integer 타입 `i32` 와 충돌해서 첫인상이 헷갈린다 (의도는 `id32` 헬퍼를 부르는 wrapper 라는 뜻이겠지만 그 정보도 호출자에게 가치 없음). 더 의도가 드러나는 이름: - `seed_below_gate_chunk` (action + condition) - `seed_low_score_chunk` (action + 성격) 동시에 함수가 cid 만 반환하면 호출자가 `id32("d_low")` 를 *다시* 만들어야 하는데 (line 410 의 `mk_hit(1, &cid, &id32("d_low"), ...)`), 이 doc_id 도 함수 안의 seed 와 같은 id32 결과라 dual source-of-truth. `(chunk_id, doc_id)` tuple 또는 바로 `SearchHit` 까지 만들어 반환하면 호출자 본문이 더 짧아지고 헬퍼-호출자 사이의 id 동기화 깨질 위험도 없어진다.
altair823 added 1 commit 2026-05-25 08:24:41 +00:00
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>
claude-reviewer-01 approved these changes 2026-05-25 08:25:14 +00:00
claude-reviewer-01 left a comment
Member

회차 3 — 회차 2 의 함수 이름 nit 완벽 반영.

i32_below_gate_chunkseed_low_score_chunk rename + (chunk_id, doc_id) tuple 반환 + 호출자가 id32("d_low") 를 재계산하지 않도록 id 페어 통합. doc 도 의도 명확하게 갱신.

추가 actionable 항목 없음 — 머지에 동의.

요약:

  • ScriptedLm + ScriptedRetriever helper 도입으로 multi-hop pipeline 의 단계별 (decompose / decide×N / synthesize) LLM call 을 mock-only 로 정밀 검증 가능.
  • 7 integration test (5 happy-path + 2 refuse-path) 가 decide stop / continue / depth cap / pool dedup / parse-degraded synthesize / NoChunks-with-hops / ScoreGate-with-hops 7 시나리오 핀.
  • refuse_no_chunks / refuse_score_gate 의 hops widening 으로 multi-hop refusal 경로도 partial hop trace 보존 — wire-additive (single-pass 영향 없음).
  • 회차 1 carry-over (HopRecord doc per-kind / llm_call_ms ambiguity / MULTI_HOP_MAX_SUB_QUERIES_HARD_CAP rename + 의도 doc / parse_decompose_response post-condition / decide preview snippet path) 모두 closure.
  • cargo test -p kebab-rag -j 1 31 unit + 19 pipeline + 7 multi_hop + 3 prompt_template + 3 streaming 통과.
  • cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings clean.
회차 3 — 회차 2 의 함수 이름 nit 완벽 반영. `i32_below_gate_chunk` → `seed_low_score_chunk` rename + `(chunk_id, doc_id)` tuple 반환 + 호출자가 `id32("d_low")` 를 재계산하지 않도록 id 페어 통합. doc 도 의도 명확하게 갱신. 추가 actionable 항목 없음 — 머지에 동의. 요약: - ScriptedLm + ScriptedRetriever helper 도입으로 multi-hop pipeline 의 단계별 (decompose / decide×N / synthesize) LLM call 을 mock-only 로 정밀 검증 가능. - 7 integration test (5 happy-path + 2 refuse-path) 가 decide stop / continue / depth cap / pool dedup / parse-degraded synthesize / NoChunks-with-hops / ScoreGate-with-hops 7 시나리오 핀. - refuse_no_chunks / refuse_score_gate 의 hops widening 으로 multi-hop refusal 경로도 partial hop trace 보존 — wire-additive (single-pass 영향 없음). - 회차 1 carry-over (HopRecord doc per-kind / llm_call_ms ambiguity / MULTI_HOP_MAX_SUB_QUERIES_HARD_CAP rename + 의도 doc / parse_decompose_response post-condition / decide preview snippet path) 모두 closure. - `cargo test -p kebab-rag -j 1` 31 unit + 19 pipeline + 7 multi_hop + 3 prompt_template + 3 streaming 통과. - `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.
altair823 merged commit 64a009314c into main 2026-05-25 08:25:44 +00:00
altair823 deleted branch feat/fb-41-pr-3b-ii-scripted-lm-tests 2026-05-25 08:25:45 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#170