feat(rag): fb-41 PR-3b-i dynamic decide loop + helpers #169

Merged
altair823 merged 1 commits from feat/fb-41-pr-3b-decide-loop into main 2026-05-25 07:32:27 +00:00
Owner

요약

fb-41 multi-hop RAG 의 PR-3b-i (PR-3b 의 분할 첫 PR, code 변경 중심). ask_multi_hop 의 fixed depth=2 → dynamic N-hop. ScriptedLm helper + 5+ integration tests 는 PR-3b-ii.

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

변경

  • multi_hop_decompose 시그니처 변경 — Result<(Option<Vec<String>>, u32)> (parsed + latency_ms). caller 가 HopRecord 의 llm_call_ms stamp.
  • multi_hop_decide helper 신규 — decide LLM call → parse_decompose_response. 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-§2 영역 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).
    • max_pool_chunks 도달: pool_cap_hit=true → forced_stop=true + decide LLM call skip.
    • max_depth 도달: forced_stop=true (마지막 iter).
    • decide parse failure 또는 empty array: loop break (early synthesize, NOT refusal — spec §9 graceful degrade).
  • §6 (Generate) 시작 시 synthesize_started 별 stamp → §8 Build Answer 직전 HopRecord { kind=Synthesize, llm_call_ms=synth_ms } 추가. happy path 의 Answer literal hops: Some(hops) 채움.
  • doc comment 갱신: "PR-2 scope (fixed depth=2)" → "PR-3b-i scope (dynamic N-hop)". refusal path 의 hops trace 손실 caveat 명시.

검증

  • cargo build -p kebab-rag -j 1 — clean.
  • cargo test -p kebab-rag -j 156 test 모두 통과. 기존 PR-2 회귀 핀 2 통과 (ask_multi_hop_dispatches_and_decompose_garbage_refuses + ask_with_multi_hop_false_keeps_single_pass_path).
  • cargo clippy -p kebab-rag -p kebab-core -p kebab-store-sqlite -p kebab-tui -p kebab-app --all-targets -j 1 -- -D warnings — clean.

시험 항목 (Test Plan)

  • 기존 PR-2 회귀 핀 (decompose garbage → MultiHopDecomposeFailed + 1 LLM call) — PR-3b-i 의 시그니처 변경 후도 통과
  • multi_hop=false single-pass keep — 영향 없음 회귀 핀
  • dynamic loop 의 build / clippy 모두 clean
  • happy path multi-hop integration test (ScriptedLm 필요) — PR-3b-ii

비범위 (PR-3b-ii)

  • ScriptedLm helper (per-call 다른 response 반환) — kebab-rag tests/common/mod.rs 또는 별 module
  • 5+ happy-path integration tests:
    • multi_hop_decide_stop_triggers_synthesize — decide [] → 즉시 synthesize
    • multi_hop_decide_continue_adds_more_chunks — decide ["q4"] → iter 2 진행
    • multi_hop_max_depth_force_stops — depth=max_depth → forced_stop=true
    • multi_hop_pool_chunks_dedup_by_chunk_id — 같은 chunk 가 두 sub-query 에서 나와도 pool 에 1 회
    • multi_hop_decide_parse_failure_falls_through_to_synthesize — graceful degrade
  • refusal path 의 hops trace 보존 (helper signature widening)
  • mirror refactor (ask + ask_multi_hop 의 §4-§9 공통 helper)
  • JSON Schema answer.schema.jsonhops 명시 추가 (additive minor)

Wire 영향

Answer.hops 가 multi-hop happy path 에서 emit. JSON Schema additionalProperties default true (PR-3a review 확인) 라 wire breaking 아님 — additive. schema.json 명시 갱신은 PR-3b-ii 또는 PR-4 의 sweep.

다음 단계

PR-3b-i 머지 후 PR-3b-ii (ScriptedLm + 5+ tests + refusal hops). 그 다음 PR-4 (CLI --multi-hop flag + wire schema additive).

Assisted-by: Claude Code

## 요약 fb-41 multi-hop RAG 의 **PR-3b-i** (PR-3b 의 분할 첫 PR, code 변경 중심). `ask_multi_hop` 의 fixed depth=2 → dynamic N-hop. ScriptedLm helper + 5+ integration tests 는 PR-3b-ii. 설계: 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 단락) ## 변경 - `multi_hop_decompose` 시그니처 변경 — `Result<(Option<Vec<String>>, u32)>` (parsed + latency_ms). caller 가 HopRecord 의 `llm_call_ms` stamp. - `multi_hop_decide` helper 신규 — decide LLM call → `parse_decompose_response`. 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-§2 영역 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). - **`max_pool_chunks` 도달**: pool_cap_hit=true → forced_stop=true + decide LLM call skip. - **`max_depth` 도달**: forced_stop=true (마지막 iter). - **decide parse failure 또는 empty array**: loop break (early synthesize, NOT refusal — spec §9 graceful degrade). - §6 (Generate) 시작 시 `synthesize_started` 별 stamp → §8 Build Answer 직전 `HopRecord { kind=Synthesize, llm_call_ms=synth_ms }` 추가. **happy path 의 Answer literal `hops: Some(hops)` 채움**. - doc comment 갱신: "PR-2 scope (fixed depth=2)" → "PR-3b-i scope (dynamic N-hop)". refusal path 의 hops trace 손실 caveat 명시. ## 검증 - `cargo build -p kebab-rag -j 1` — clean. - `cargo test -p kebab-rag -j 1` — **56 test 모두 통과**. 기존 PR-2 회귀 핀 2 통과 (`ask_multi_hop_dispatches_and_decompose_garbage_refuses` + `ask_with_multi_hop_false_keeps_single_pass_path`). - `cargo clippy -p kebab-rag -p kebab-core -p kebab-store-sqlite -p kebab-tui -p kebab-app --all-targets -j 1 -- -D warnings` — clean. ## 시험 항목 (Test Plan) - [x] 기존 PR-2 회귀 핀 (decompose garbage → MultiHopDecomposeFailed + 1 LLM call) — PR-3b-i 의 시그니처 변경 후도 통과 - [x] multi_hop=false single-pass keep — 영향 없음 회귀 핀 - [x] dynamic loop 의 build / clippy 모두 clean - [ ] **happy path multi-hop integration test** (ScriptedLm 필요) — PR-3b-ii ## 비범위 (PR-3b-ii) - `ScriptedLm` helper (per-call 다른 response 반환) — kebab-rag tests/common/mod.rs 또는 별 module - 5+ happy-path integration tests: - `multi_hop_decide_stop_triggers_synthesize` — decide `[]` → 즉시 synthesize - `multi_hop_decide_continue_adds_more_chunks` — decide `["q4"]` → iter 2 진행 - `multi_hop_max_depth_force_stops` — depth=max_depth → forced_stop=true - `multi_hop_pool_chunks_dedup_by_chunk_id` — 같은 chunk 가 두 sub-query 에서 나와도 pool 에 1 회 - `multi_hop_decide_parse_failure_falls_through_to_synthesize` — graceful degrade - refusal path 의 hops trace 보존 (helper signature widening) - mirror refactor (`ask` + `ask_multi_hop` 의 §4-§9 공통 helper) - JSON Schema `answer.schema.json` 의 `hops` 명시 추가 (additive minor) ## Wire 영향 `Answer.hops` 가 multi-hop happy path 에서 emit. JSON Schema `additionalProperties` default `true` (PR-3a review 확인) 라 wire breaking 아님 — additive. schema.json 명시 갱신은 PR-3b-ii 또는 PR-4 의 sweep. ## 다음 단계 PR-3b-i 머지 후 PR-3b-ii (ScriptedLm + 5+ tests + refusal hops). 그 다음 PR-4 (CLI `--multi-hop` flag + wire schema additive). Assisted-by: Claude Code
altair823 added 1 commit 2026-05-25 07:30:18 +00:00
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>
claude-reviewer-01 approved these changes 2026-05-25 07:32:13 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — dynamic decide loop 의 step (decompose → retrieve+decide loop → synthesize) 가 명확. forced_stop signaling unified, decide parse failure 의 graceful degrade 정확. actionable 0 + suggestion 3 + 칭찬 2:

suggestion (모두 PR-3b-ii 또는 follow-up):

  • decide preview 의 pool snippet concat 가 cfg.rag.max_context_tokens 와 별 cap — multi_hop_max_pool_chunks 가 늘면 overflow 가능. pack budget 적용 검토
  • HopRecord.llm_call_ms = 0 의 모호함 ("no call" vs "0ms call"). doc 명시 또는 Option
  • Some(qs) if !qs.is_empty() guard 의 의도 (defensive vs dead code) — doc 명시

칭찬:

  • forced_stop 의 unified signal (depth + pool cap 같은 패턴) — wire 단순성 + archeology 자연
  • doc comment 의 refusal trace 손실 caveat 명시 — staging 의도 추적 가능

APPROVE — 본 PR scope 안에서 의도 분명, 모든 회귀 핀 통과. PR-3b-ii (ScriptedLm + 5+ happy-path integration tests) 진행 권장.

회차 1 — dynamic decide loop 의 step (decompose → retrieve+decide loop → synthesize) 가 명확. forced_stop signaling unified, decide parse failure 의 graceful degrade 정확. actionable 0 + suggestion 3 + 칭찬 2: suggestion (모두 PR-3b-ii 또는 follow-up): - decide preview 의 pool snippet concat 가 cfg.rag.max_context_tokens 와 별 cap — multi_hop_max_pool_chunks 가 늘면 overflow 가능. pack budget 적용 검토 - HopRecord.llm_call_ms = 0 의 모호함 ("no call" vs "0ms call"). doc 명시 또는 Option<u32> - `Some(qs) if !qs.is_empty()` guard 의 의도 (defensive vs dead code) — doc 명시 칭찬: - forced_stop 의 unified signal (depth + pool cap 같은 패턴) — wire 단순성 + archeology 자연 - doc comment 의 refusal trace 손실 caveat 명시 — staging 의도 추적 가능 APPROVE — 본 PR scope 안에서 의도 분명, 모든 회귀 핀 통과. PR-3b-ii (ScriptedLm + 5+ happy-path integration tests) 진행 권장.
@@ -595,55 +596,143 @@ impl RagPipeline {
/// sub-questions, retrieve each separately, then synthesize a single
/// answer over the deduplicated pool.

칭찬 — doc comment 의 "refusal path 의 hops trace 손실 caveat" 명시 ("Pool empty after retrieval → RefusalReason::NoChunks (refusal path currently loses the partial hop trace — cleanup deferred to PR-3b-ii)"). 후속 reviewer 가 ask_multi_hop 의 refusal 케이스 trace 부재 즉시 발견. 의도된 staging vs forgotten work 구분 자연.

또 "decide parse failure ≠ refusal" 별도 단락 으로 graceful degrade 정책 명시 — spec §9 의 의도가 doc 에 etched 됨.

**칭찬** — doc comment 의 "refusal path 의 hops trace 손실 caveat" 명시 ("Pool empty after retrieval → `RefusalReason::NoChunks` (refusal path currently loses the partial hop trace — cleanup deferred to PR-3b-ii)"). 후속 reviewer 가 ask_multi_hop 의 refusal 케이스 trace 부재 즉시 발견. 의도된 staging vs forgotten work 구분 자연. 또 "decide parse failure ≠ refusal" 별도 단락 으로 graceful degrade 정책 명시 — spec §9 의 의도가 doc 에 etched 됨.
@@ -647,0 +714,4 @@
match decide_result {
Some(qs) if !qs.is_empty() => (qs, ms),
// Empty array OR parse failure → stop signal.
// Parse failure is NOT a refusal — graceful

Suggestion — decide prompt 의 pool snippet concat 가 pack budget 미적용:

let preview = pool
    .iter()
    .enumerate()
    .map(|(i, h)| format!("[{}] {}", i + 1, h.snippet))
    .collect::<Vec<_>>()
    .join("\n\n");

pool.iter() 가 pool 의 모든 chunk 를 concat. multi_hop_max_pool_chunks default 30 + snippet ~220 char → ~6.6 KB. multi_hop_max_pool_chunks 가 cfg 로 늘어나면 (예: 100) preview 가 ~22 KB → LLM context 의 큰 부분 차지.

cfg.rag.max_context_tokens (default 8000 ≈ 32 KB) 와 별 cap 이라 향후 overflow 가능. pack_context 의 budget 처리 (token estimate + budget trim) 와 다른 path.

옵션 (PR-3b-ii 또는 follow-up):

  1. multi_hop_decide 가 pack budget 적용 — 신규 helper pack_decide_preview(pool, budget).
  2. preview 의 chunk 수를 pool.iter().take(N) 으로 hard cap — N 도 cfg.
  3. 현재 (모든 pool) 유지 — max_pool_chunks cfg 가 implicit cap 으로 충분.

PR-3b-i 의 scope 외 — default 30 에서는 acceptable.

**Suggestion — decide prompt 의 pool snippet concat 가 pack budget 미적용**: ```rust let preview = pool .iter() .enumerate() .map(|(i, h)| format!("[{}] {}", i + 1, h.snippet)) .collect::<Vec<_>>() .join("\n\n"); ``` `pool.iter()` 가 pool 의 모든 chunk 를 concat. `multi_hop_max_pool_chunks` default 30 + snippet ~220 char → ~6.6 KB. `multi_hop_max_pool_chunks` 가 cfg 로 늘어나면 (예: 100) preview 가 ~22 KB → LLM context 의 큰 부분 차지. `cfg.rag.max_context_tokens` (default 8000 ≈ 32 KB) 와 별 cap 이라 향후 overflow 가능. `pack_context` 의 budget 처리 (token estimate + budget trim) 와 다른 path. 옵션 (PR-3b-ii 또는 follow-up): 1. `multi_hop_decide` 가 pack budget 적용 — 신규 helper `pack_decide_preview(pool, budget)`. 2. preview 의 chunk 수를 `pool.iter().take(N)` 으로 hard cap — N 도 cfg. 3. 현재 (모든 pool) 유지 — `max_pool_chunks` cfg 가 implicit cap 으로 충분. PR-3b-i 의 scope 외 — default 30 에서는 acceptable.
@@ -647,0 +717,4 @@
// Parse failure is NOT a refusal — graceful
// degrade to early synthesize (per spec §9).
_ => (Vec::new(), ms),
}

칭찬forced_stop = depth_force_stop || pool_cap_hit 의 unified signal. 두 cap (max_depth + max_pool_chunks) 가 동일 forced_stop=true 로 HopRecord 에 trace. 사용자 / agent 가 후속 archeology 시 forced_stop=true 만 보고 "cap 도달 = trace 분석 필요" 즉시 파악. 두 cap 의 별 signal 분리하지 않은 게 wire 의 단순성 ↑.

또 doc comment 의 "both early exits flag forced_stop = true on the iter's HopRecord" 명시 가 후속 reviewer 의 의도 추적 가능.

**칭찬** — `forced_stop = depth_force_stop || pool_cap_hit` 의 unified signal. 두 cap (`max_depth` + `max_pool_chunks`) 가 동일 `forced_stop=true` 로 HopRecord 에 trace. 사용자 / agent 가 후속 archeology 시 forced_stop=true 만 보고 "cap 도달 = trace 분석 필요" 즉시 파악. 두 cap 의 별 signal 분리하지 않은 게 wire 의 단순성 ↑. 또 doc comment 의 "both early exits flag `forced_stop = true` on the iter's `HopRecord`" 명시 가 후속 reviewer 의 의도 추적 가능.

Suggestion — HopRecord.llm_call_ms = 0 의미 ambiguity:

let (new_sub_queries, decide_ms): (Vec<String>, u32) =
    if forced_stop || pool.is_empty() {
        (Vec::new(), 0)  // ← "no call" sentinel
    } else {
        // ... actual call ...
        match decide_result {
            Some(qs) if !qs.is_empty() => (qs, ms),
            _ => (Vec::new(), ms),
        }
    };

HopRecord 의 llm_call_ms = 0 가 두 의미 hold:

  1. No call — forced_stop 또는 pool empty 라서 decide LLM call skip.
  2. 0ms call — 실제 LLM call 했지만 1ms 미만 (instrumentation precision 문제).

실제 (2) 거의 안 일어남 (LLM call 은 항상 latency 있음). 단 후속 archeology 가 llm_call_ms = 0 보면 두 케이스 구분 어려움.

옵션:

  1. doc 명시: HopRecord doc 에 "forced_stop = true + llm_call_ms = 0 = decide call skipped" 안내. PR-3b-ii 단계에서 추가.
  2. Option<u32>: shape 변경 — llm_call_ms: Option<u32> 로 None 가 "no call". 단 wire schema 영향 + serde 다룸. PR-3b-ii 또는 future PR scope.

PR-3b-i 의 scope 외 — 의미가 trace 의 fidelity 측면 약점.

**Suggestion — `HopRecord.llm_call_ms = 0` 의미 ambiguity**: ```rust let (new_sub_queries, decide_ms): (Vec<String>, u32) = if forced_stop || pool.is_empty() { (Vec::new(), 0) // ← "no call" sentinel } else { // ... actual call ... match decide_result { Some(qs) if !qs.is_empty() => (qs, ms), _ => (Vec::new(), ms), } }; ``` HopRecord 의 `llm_call_ms = 0` 가 두 의미 hold: 1. **No call** — forced_stop 또는 pool empty 라서 decide LLM call skip. 2. **0ms call** — 실제 LLM call 했지만 1ms 미만 (instrumentation precision 문제). 실제 (2) 거의 안 일어남 (LLM call 은 항상 latency 있음). 단 후속 archeology 가 `llm_call_ms = 0` 보면 두 케이스 구분 어려움. 옵션: 1. **doc 명시**: HopRecord doc 에 "`forced_stop = true` + `llm_call_ms = 0` = decide call skipped" 안내. PR-3b-ii 단계에서 추가. 2. **`Option<u32>`**: shape 변경 — `llm_call_ms: Option<u32>` 로 None 가 "no call". 단 wire schema 영향 + serde 다룸. PR-3b-ii 또는 future PR scope. PR-3b-i 의 scope 외 — 의미가 trace 의 fidelity 측면 약점.

Suggestion — Some(qs) if !qs.is_empty() guard 의 의도 doc:

match decide_result {
    Some(qs) if !qs.is_empty() => (qs, ms),
    _ => (Vec::new(), ms),
}

현재 parse_decompose_response 의 documented invariant: empty array → None (drop empty + take cap + None on all-empty). 따라서 decide_resultSome(...) 면 항상 non-empty array. 즉 if !qs.is_empty() guard 가 실질적 dead code.

Defensive 의도 (parse_decompose_response 가 미래에 invariant 변경 시 안전) 면 OK — doc comment 한 줄로 명시 권장:

match decide_result {
    // Defensive: parse_decompose_response 가 None 으로 collapse 하지만,
    // 만약 future invariant 변경으로 Some(empty) emit 시 stop 신호로
    // 동일하게 처리.
    Some(qs) if !qs.is_empty() => (qs, ms),
    _ => (Vec::new(), ms),
}

또는 단순화: decide_result.unwrap_or_default() + if qs.is_empty() { break } 한 단계 더 위에서. Suggestion 만.

**Suggestion — `Some(qs) if !qs.is_empty()` guard 의 의도 doc**: ```rust match decide_result { Some(qs) if !qs.is_empty() => (qs, ms), _ => (Vec::new(), ms), } ``` 현재 `parse_decompose_response` 의 documented invariant: empty array → None (drop empty + take cap + None on all-empty). 따라서 `decide_result` 가 `Some(...)` 면 항상 non-empty array. 즉 `if !qs.is_empty()` guard 가 실질적 dead code. Defensive 의도 (parse_decompose_response 가 미래에 invariant 변경 시 안전) 면 OK — doc comment 한 줄로 명시 권장: ```rust match decide_result { // Defensive: parse_decompose_response 가 None 으로 collapse 하지만, // 만약 future invariant 변경으로 Some(empty) emit 시 stop 신호로 // 동일하게 처리. Some(qs) if !qs.is_empty() => (qs, ms), _ => (Vec::new(), ms), } ``` 또는 단순화: `decide_result.unwrap_or_default()` + `if qs.is_empty() { break }` 한 단계 더 위에서. Suggestion 만.
altair823 merged commit 94e6146013 into main 2026-05-25 07:32:27 +00:00
altair823 deleted branch feat/fb-41-pr-3b-decide-loop 2026-05-25 07:32:28 +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#169