feat(cli): kebab ingest progress display (p9-fb-02) + p9-fb-01 status flip #53
Reference in New Issue
Block a user
Delete Branch "feat/p9-fb-02-cli-progress"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
kebab ingest의 진행 표시 — spec PR #51 의 §2.4a (ingest_progress.v1) 와 p9-fb-01 의 facade callback 위에 build. 도그푸딩 막힘 강도 2위 (1.8 초 묵음 → hung vs empty 구분 불가) 해소. 동일 PR 에서 p9-fb-01 spec 의 status flip 도 처리.변경
crates/kebab-cli/src/progress.rs:ProgressMode::{Json, Human{tty}}+ProgressDisplaystruct. background thread 가 channel drain + 모드별 render.crates/kebab-cli/src/wire.rs:wire_ingest_progresshelper — serde 의kinddiscriminator 위에schema_version+ts(RFC 3339, emit 시점 stamp) 추가.Cmd::Ingesthandler: mpsc channel + spawned display thread +ingest_with_config_progress(.., Some(tx)). ingest 반환 시 Sender drop → display thread 정상 종료. join 후 최종 ingest_report 출력.indicatif = "0.17". TTY 전용 진행 바, non-TTY/--json에서는 hidden draw target.kebab ingest행 갱신 (TTY / non-TTY / --json 분기 명시) + wire schema 예시 목록 갱신.p9-fb-01statusin_progress→completed(PR #52 follow-up).p9-fb-02statusplanned→in_progress. 머지 후 한 줄 commit 으로p9-fb-02도completed.행동
세 모드:
Test plan
cargo test -p kebab-cli— 16 PASS (9 wire unit + 3 ingest_progress_cli + 4 reset_cli).cargo clippy -p kebab-cli --all-targets -- -D warningsclean.--jsonline-delimited stream / non-TTY stderr text /ts+kind검증.미적용 (별 task)
IngestEvent::Aborted발신 + Ctrl-C cancel handler → p9-fb-04후속
p9-fb-02statusin_progress→completed한 줄 commit.`kebab ingest` 가 진행 상황을 사용자에게 보여주는 두 surface 추가: - **사람 모드 (TTY)**: indicatif `ProgressBar` on stderr — scan 중에는 spinner, ScanCompleted 후 bar 로 전환, 매 asset 마다 message 갱신. - **사람 모드 (non-TTY, CI/pipe)**: indicatif draw target 을 hidden 으로 두고 stderr 에 한 줄씩 (`ingest: scanning`, `ingest: 1/N path`, `ingest: complete (...)`). - **`--json` 모드**: stderr 비우고 stdout 에 line-delimited `ingest_progress.v1` JSON 을 emit. 마지막 줄은 기존 `ingest_report.v1` 그대로 (외부 wrapper backward-compat). 구현: - 신규 `crates/kebab-cli/src/progress.rs` — `ProgressMode::{Json, Human { tty }}`, `ProgressDisplay` (background thread 가 channel drain + 모드별 render), `now_rfc3339` helper. mode 가 무엇이든 ts 는 wire emit 시점에 stamp. - `crates/kebab-cli/src/wire.rs` 에 `wire_ingest_progress` 추가. serde tag (`kind`) 위에 `schema_version` + `ts` 두 필드 더해 spec §2.4a wire shape 완성. - `Cmd::Ingest` 핸들러: mpsc channel 만들고 background thread 가 display 돌리는 동안 main 이 `ingest_with_config_progress` 호출. ingest 반환 시 Sender drop → display thread 정상 종료. join 후 최종 ingest_report 출력. - 새 dep: `indicatif` 0.17 (TTY 전용 진행 바, non-TTY/--json 에서는 hidden draw target). Test: - 3 lib unit (mode resolution + RFC 3339 round-trip). - 3 integration (--json line-delimited / non-TTY stderr text / ts+kind 검증). 16 PASS 전체 회귀 0. Plan 갱신: - p9-fb-01: status `in_progress` → `completed` (PR #52 머지 후속). - p9-fb-02: status `planned` → `in_progress`. 머지 후 별도 한 줄 commit 으로 `completed` flip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — nit 3건 + 칭찬 3건. 코드 변경 사이즈 ~450 LOC, 3 surface (TTY / non-TTY / --json) 모두 cover.
핵심 actionable (모두 의도 문서화):
let _ = writeln!(err, ...)가 io error swallow — best-effort 의도 명시 한 줄.bar.as_ref()None 분기 silent skip — §2.4a ordering invariant 의존성 코멘트.let _ = display_handle.join()— display 실패가 ingest exit code 에 영향 없는 의도 trailing comment.총평: spec PR #51 + p9-fb-01 (PR #52) 위에 깔끔한 3-layer (spec → facade → CLI). background thread + channel-drop-based clean exit 패턴 단순. integration test 가 §2.4a 의 backward-compat 계약 (마지막 줄 = ingest_report.v1) 를 코드화. 위 nit 3 건은 모두 의도 문서화 — 동작 변경 없음.
@@ -287,0 +287,4 @@// p9-fb-02: spawn the progress display on a background// thread; the ingest call below holds the `Sender` end of// the channel and emits per-step events into it. When the(칭찬) main thread 가 ingest 호출 + background thread 가 channel drain — Sender drop 시 receiver loop 자연 종료. 별도 shutdown signal / Mutex 없이 단순.
display_handle.join()도 ingest 종료 후 즉시 도달 (Sender drop 동기). cancel-token 같은 명시적 sync primitive 없이 channel 자체가 lifecycle 관리.@@ -287,0 +306,4 @@// Join the display thread *before* surfacing the ingest// outcome so the spinner / final newline is flushed// regardless of whether ingest returned Ok or Err.let _ = display_handle.join();(nit / 의도 문서화)
let _ = display_handle.join();가JoinHandle::join()의Result<Result<(), anyhow::Error>, Box<dyn Any>>를 모두 swallow. display thread 가 panic 했거나 emit 실패해도 ingest exit code 에 영향 없음 — 의도이지만 명시 안 됨.Why: display panic 이 cli 에 노출 안 되면 CI 에서 progress 버그 발견 어려움. 다만 ingest 자체는 ok 인 상태에서 display 실패로 exit 1 만들면 user-facing 회귀 더 큼 — 의도적 swallow 정합.
How to apply: 같은 줄에 한 줄 trailing comment —
// display thread errors are best-effort; ingest exit code 는 ingest_result 만 결정.@@ -0,0 +87,4 @@}}fn handle_human(&mut self, event: &IngestEvent, tty: bool) -> anyhow::Result<()> {(칭찬)
ProgressModeenum 이 surface 별 variant —Json/Human { tty }.from_flags가--json우선 + tty detection 위임 → handler 안에서 모드 분기가 자연. PR body 의 세 모드 (TTY / non-TTY / --json) 이 enum 선언 한 곳에서 한 눈에 보임.@@ -0,0 +100,4 @@self.bar = Some(bar);if !tty {let mut err = std::io::stderr().lock();let _ = writeln!(err, "ingest: scanning {root}…");(nit / 의도 문서화) 매
let _ = writeln!(err, ...)가 io error 를 silent swallow 함 — progress UI 가 실패해도 ingest 자체는 이어가야 한다는 의도이지만 코드 옆에서 명시 안 됨. 4 곳 (ScanStarted / ScanCompleted / AssetStarted / Completed / Aborted) 동일 패턴.Why:
let _ =는 "의도적으로 무시" 신호이지만 무엇을 무시하는지 + 왜 OK 인지 도드라지지 않음. 미래 reader 가 "왜 ? 안 쓰나" 의문 생김.How to apply: handle_human 함수 doc-comment 에 한 줄 추가 — "All
writeln!calls into stderr swallow IO errors: progress display is best-effort and must not fail the ingest run if the terminal is closed mid-stream."@@ -0,0 +128,4 @@path,media,} => {if let Some(bar) = self.bar.as_ref() {(invariant 코멘트)
bar.as_ref()분기에서bar가 None 이면 silent skip — AssetStarted / AssetFinished 둘 다. ScanStarted 가 항상 첫 event 라는 §2.4a invariant 에 의존. p9-fb-04 의 cancel 이 매우 빠르면 (?) AssetStarted 가 ScanStarted 없이 도착할 수 있을지 — 현재 spec 으로는 불가능 (Aborted는 ScanStarted 후에만 발신).Why: 미래 누군가 emit 위치를 옮기거나 새 variant 를 추가하면 silent drop 될 수 있음. invariant 가 코드 옆에서 명시되면 회귀 추적 ↑.
How to apply: AssetStarted 분기 위에
// Invariant:self.bar는 ScanStarted 처리 시점에 항상 Some. None 분기는 spec §2.4a 의 ordering invariant (ScanStarted < AssetStarted) 가 깨졌을 때만 도달 — 현재는 unreachable 이지만 panic 보다는 silent skip 으로 graceful degrade.정도 한 줄.(칭찬)
progress_seen >= 4+last_schema == ingest_report.v1두 invariant 가 §2.4a 의 "마지막 줄은 기존 ingest_report.v1 그대로 (외부 wrapper backward-compat)" 계약을 코드로 코드화. 미래 누군가 progress 와 report 순서 뒤집거나 schema_version 누락 시 즉시 잡힘 — 외부 wrapper (Claude Code skill / MCP) 의 회귀 안전망.회차 2 — 회차 1 nit 3건 (handle_human doc-comment / bar invariant / display join trailing comment) 모두 정확히 반영. 동작 변경 없음, 의도 명시만. 추가 actionable 0. APPROVE.