feat(cli): kebab ask citation block (p9-fb-20)

답변 출력 후 `근거:` 절 — `[N] <full path>#<fragment>  (score=<s>)`
한 줄씩. spec p9-fb-20 의 핵심 (full path 가독성) 충족.

신규 flag:
- --show-citations: default ON. 답변 후 citation block 출력.
- --hide-citations: 답변 본문만 출력 (pipe 시 다른 도구가 trailing
  metadata 안 받기 원할 때).

`--json` 모드 무영향 — citations 가 wire payload 에 항상 포함되므로
flag 가 영향 X (외부 wrapper 호환성).

spec p9-fb-20 의 \"TUI citation pane + jump (Enter/o editor jump,
i inspect)\" 부분은 본 PR scope 에서 제외 — TUI 의 기존
render_citations_or_explain (P9-3) 가 이미 citation list 표시,
추가 fold/jump 는 후속 task. 사용자 도그푸딩 priority 5위 의
핵심 = \"full path 가독성\" 이라 CLI block 만으로 충분.

Plan 갱신:
- p9-fb-20 status planned → in_progress. 머지 후 한 줄 commit
  으로 completed flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 00:37:47 +00:00
parent d3a90bdd7b
commit c1555f3c50
4 changed files with 39 additions and 2 deletions

View File

@@ -99,6 +99,21 @@ enum Cmd {
#[arg(long)]
seed: Option<u64>,
/// p9-fb-20: print the `근거:` block (full path / line range
/// / score, one per line) after the answer. Default on.
/// `--json` mode is unaffected — citations are always
/// included in the wire payload regardless of this flag.
#[arg(long, action = clap::ArgAction::SetTrue,
conflicts_with = "hide_citations",
default_value_t = true)]
show_citations: bool,
/// p9-fb-20: opt out of the `근거:` block (sticky-overrides
/// `--show-citations`). Useful when piping the answer body
/// to another tool that doesn't want trailing metadata.
#[arg(long)]
hide_citations: bool,
},
/// Wipe XDG data dirs (and optionally the Lance vector store) so the
@@ -418,6 +433,8 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
explain,
temperature,
seed,
show_citations,
hide_citations,
} => {
let cfg = kebab_config::Config::load(cli.config.as_deref())?;
let opts = kebab_app::AskOpts {
@@ -442,6 +459,25 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
println!("{}", serde_json::to_string(&wire::wire_answer(&ans))?);
} else {
println!("{}", ans.answer);
// p9-fb-20: print the citation block after the
// answer body when --hide-citations is not set
// (--show-citations is the default). Skipped on
// refusal-with-zero-citations to avoid an empty
// `근거:` header.
let print_citations = *show_citations && !*hide_citations;
if print_citations && !ans.citations.is_empty() {
println!();
println!("근거:");
for (idx, c) in ans.citations.iter().enumerate() {
let marker = c.marker.as_deref().unwrap_or(&format!("{}", idx + 1)).to_string();
println!(
" [{}] {} (score={:.2})",
marker,
c.citation.to_uri(),
ans.retrieval.top_score,
);
}
}
}
// Refusal → exit 1.
if !ans.grounded {