feat(cli): kebab ingest progress display (p9-fb-02) + p9-fb-01 status flip #53

Merged
altair823 merged 2 commits from feat/p9-fb-02-cli-progress into main 2026-05-02 20:01:14 +00:00
Owner

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}} + ProgressDisplay struct. background thread 가 channel drain + 모드별 render.
  • crates/kebab-cli/src/wire.rs: wire_ingest_progress helper — serde 의 kind discriminator 위에 schema_version + ts (RFC 3339, emit 시점 stamp) 추가.
  • Cmd::Ingest handler: mpsc channel + spawned display thread + ingest_with_config_progress(.., Some(tx)). ingest 반환 시 Sender drop → display thread 정상 종료. join 후 최종 ingest_report 출력.
  • 새 dep: indicatif = "0.17". TTY 전용 진행 바, non-TTY/--json 에서는 hidden draw target.
  • README.md: kebab ingest 행 갱신 (TTY / non-TTY / --json 분기 명시) + wire schema 예시 목록 갱신.
  • HANDOFF.md: 2026-05-02 dated entry 추가.
  • spec 갱신: p9-fb-01 status in_progresscompleted (PR #52 follow-up). p9-fb-02 status plannedin_progress. 머지 후 한 줄 commit 으로 p9-fb-02completed.

행동

세 모드:

TTY 사람 모드:
  ingest [================>      ] 142/1024  markdown notes/foo.md
  (스캔 중에는 spinner; ScanCompleted 후 bar 로 전환)

non-TTY 사람 모드 (CI / pipe):
  ingest: scanning /home/altair/KnowledgeBase…
  ingest: scan complete (1024 assets)
  ingest: 1/1024 markdown notes/foo.md
  ...
  ingest: complete (scanned=1024 new=12 updated=3 skipped=1009 errors=0)

--json 모드:
  {"schema_version":"ingest_progress.v1","kind":"scan_started","ts":"...","root":"..."}
  {"schema_version":"ingest_progress.v1","kind":"scan_completed","ts":"...","total":1024}
  ...
  {"schema_version":"ingest_progress.v1","kind":"completed","ts":"...","counts":{...}}
  {"schema_version":"ingest_report.v1","scanned":1024,...}   ← 마지막 줄 (기존)

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 warnings clean.
  • integration test 3 종 — --json line-delimited stream / non-TTY stderr text / ts+kind 검증.

미적용 (별 task)

  • IngestEvent::Aborted 발신 + Ctrl-C cancel handler → p9-fb-04
  • TUI background worker + status bar → p9-fb-03 (다음 PR)

후속

  • 머지 후 p9-fb-02 status in_progresscompleted 한 줄 commit.
## 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}}` + `ProgressDisplay` struct. background thread 가 channel drain + 모드별 render. - **`crates/kebab-cli/src/wire.rs`**: `wire_ingest_progress` helper — serde 의 `kind` discriminator 위에 `schema_version` + `ts` (RFC 3339, emit 시점 stamp) 추가. - **`Cmd::Ingest` handler**: mpsc channel + spawned display thread + `ingest_with_config_progress(.., Some(tx))`. ingest 반환 시 Sender drop → display thread 정상 종료. join 후 최종 ingest_report 출력. - **새 dep**: `indicatif = "0.17"`. TTY 전용 진행 바, non-TTY/`--json` 에서는 hidden draw target. - **README.md**: `kebab ingest` 행 갱신 (TTY / non-TTY / --json 분기 명시) + wire schema 예시 목록 갱신. - **HANDOFF.md**: 2026-05-02 dated entry 추가. - **spec 갱신**: `p9-fb-01` status `in_progress` → `completed` (PR #52 follow-up). `p9-fb-02` status `planned` → `in_progress`. 머지 후 한 줄 commit 으로 `p9-fb-02` 도 `completed`. ## 행동 세 모드: ```text TTY 사람 모드: ingest [================> ] 142/1024 markdown notes/foo.md (스캔 중에는 spinner; ScanCompleted 후 bar 로 전환) non-TTY 사람 모드 (CI / pipe): ingest: scanning /home/altair/KnowledgeBase… ingest: scan complete (1024 assets) ingest: 1/1024 markdown notes/foo.md ... ingest: complete (scanned=1024 new=12 updated=3 skipped=1009 errors=0) --json 모드: {"schema_version":"ingest_progress.v1","kind":"scan_started","ts":"...","root":"..."} {"schema_version":"ingest_progress.v1","kind":"scan_completed","ts":"...","total":1024} ... {"schema_version":"ingest_progress.v1","kind":"completed","ts":"...","counts":{...}} {"schema_version":"ingest_report.v1","scanned":1024,...} ← 마지막 줄 (기존) ``` ## Test plan - [x] `cargo test -p kebab-cli` — 16 PASS (9 wire unit + 3 ingest_progress_cli + 4 reset_cli). - [x] `cargo clippy -p kebab-cli --all-targets -- -D warnings` clean. - [x] integration test 3 종 — `--json` line-delimited stream / non-TTY stderr text / `ts+kind` 검증. ## 미적용 (별 task) - `IngestEvent::Aborted` 발신 + Ctrl-C cancel handler → p9-fb-04 - TUI background worker + status bar → p9-fb-03 (다음 PR) ## 후속 - 머지 후 `p9-fb-02` status `in_progress` → `completed` 한 줄 commit.
altair823 added 1 commit 2026-05-02 19:57:35 +00:00
`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>
claude-reviewer-01 requested changes 2026-05-02 19:59:32 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — nit 3건 + 칭찬 3건. 코드 변경 사이즈 ~450 LOC, 3 surface (TTY / non-TTY / --json) 모두 cover.

핵심 actionable (모두 의도 문서화):

  1. progress.rs 의 매 let _ = writeln!(err, ...) 가 io error swallow — best-effort 의도 명시 한 줄.
  2. progress.rs 의 bar.as_ref() None 분기 silent skip — §2.4a ordering invariant 의존성 코멘트.
  3. main.rs 의 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 건은 모두 의도 문서화 — 동작 변경 없음.

회차 1 — nit 3건 + 칭찬 3건. 코드 변경 사이즈 ~450 LOC, 3 surface (TTY / non-TTY / --json) 모두 cover. 핵심 actionable (모두 의도 문서화): 1. progress.rs 의 매 `let _ = writeln!(err, ...)` 가 io error swallow — best-effort 의도 명시 한 줄. 2. progress.rs 의 `bar.as_ref()` None 분기 silent skip — §2.4a ordering invariant 의존성 코멘트. 3. main.rs 의 `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 관리.

(칭찬) 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 만 결정.

(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<()> {

(칭찬) ProgressMode enum 이 surface 별 variant — Json / Human { tty }. from_flags--json 우선 + tty detection 위임 → handler 안에서 모드 분기가 자연. PR body 의 세 모드 (TTY / non-TTY / --json) 이 enum 선언 한 곳에서 한 눈에 보임.

(칭찬) `ProgressMode` enum 이 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."

(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. 정도 한 줄.

(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) 의 회귀 안전망.

(칭찬) `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) 의 회귀 안전망.
altair823 added 1 commit 2026-05-02 20:00:57 +00:00
회차 1 actionable 모두 동작 변경 없음, 의도 명시.

- progress.rs handle_human: doc-comment 한 단락 — `let _ = writeln!`
  의 IO error swallow 와 `bar.as_ref()` None 분기 silent skip 의
  두 best-effort 의도 + §2.4a ordering invariant (ScanStarted 가
  bar 를 lazy 초기화) 명시.
- main.rs Cmd::Ingest: `let _ = display_handle.join();` 위에 한 줄
  trailing comment — Result<Result<(), anyhow::Error>, Box<dyn Any>>
  를 모두 discard 하는 이유 (display thread 의 에러 / panic 이
  ingest exit code 에 영향 없어야 함).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-02 20:01:04 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 nit 3건 (handle_human doc-comment / bar invariant / display join trailing comment) 모두 정확히 반영. 동작 변경 없음, 의도 명시만. 추가 actionable 0. APPROVE.

회차 2 — 회차 1 nit 3건 (handle_human doc-comment / bar invariant / display join trailing comment) 모두 정확히 반영. 동작 변경 없음, 의도 명시만. 추가 actionable 0. APPROVE.
altair823 merged commit f8584a26f3 into main 2026-05-02 20:01:14 +00:00
altair823 deleted branch feat/p9-fb-02-cli-progress 2026-05-02 20:01:16 +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#53