feat(app): #228 삭제 sweep 을 진행바·ndjson 로그에 노출 #236
Reference in New Issue
Block a user
Delete Branch "feat/sweep-progress-events"
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?
요약
sweep_deleted_files는 walker 가 끝난 직후, asset 루프가 시작되기 전에 돈다. 이 구간이 관측 가능한 신호를 하나도 내지 않았다. 진행바는 walker 총계(0/12115)를 표시한 채 멈춰 보이고, ndjson 로그는 0바이트로 남는다.tracing::info!은 나가지만 wire 이벤트가 아니라 사용자가 볼 산출물이 없다.프로세스는 CPU 100% 로 정상 동작 중인데 밖에서는 hang 과 구별할 수 없다. 이슈가 보고한 대로 실제 도그푸딩에서 세 번 연속 Ctrl-C 로 죽였다.
ingest_progress.v1에 세 이벤트를 추가한다 (additive — 기존 소비자는 모르는kind를 무시한다):sweep_startedtotalsweep_progressidx,total,path,removedsweep_completedchecked,purged,mstotal은all_workspace_paths()에서 이번 스캔이 덮은 경로를 뺀 값이라 루프 진입 전에 확정 분모가 나온다 (이슈 제안 1 그대로).removed는 "정말 없어서 문서를 지웠다" 와 "아직 디스크에 있어 그대로 뒀다" 를 가른다 — 수천 건을 훑고 하나도 안 지우는 sweep 이 있으므로, 지울 때만 움직이는 진행바는 바로 그 경우에 다시 멈춰 보인다.purged를 두 이벤트에서 다른 타입으로 쓰지 않으려고sweep_progress쪽은removed(bool),sweep_completed쪽은purged(정수) 로 이름을 나눴다. 같은 wire 키가 두 타입을 갖는 건 소비자 입장에서 함정이다.ndjson 로그에는
purge { ts, doc_path }와sweep_summary { ts, checked, purged, ms }를 추가한다 (이슈 제안 2 가 요청한 형태). 로그가 유일한 사후 기록이다 — tracing 은 stderr 로 흘러가고 남지 않는다.CLI 는 sweep 을 asset 진행바와 별 phase 로 그린다. 후보 12k 를 훑는 일과 asset 12k 를 색인하는 일은 분모가 다른 별개의 작업이라, 카운터를 공유하면 두 번째 구간이 처음부터 다시 시작하는 것처럼 보인다. 비-TTY 는 실제로 지운 것만 줄로 찍는다 — 검사한 후보마다 한 줄이면 그대로 둔 경로들이 run 의 진짜 출력을 덮는다.
설계:
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md§2.4a곁다리 — clippy 게이트가 붉었다
cargo clippy --workspace --all-targets -- -D warnings가 main 에서 실패하고 있었다. 툴체인이 올라가면서 새 lint 둘(question_mark,manual_assert_eq)이 기존 코드에 걸린 것이고 각각 한 줄이다.방치하면 안 되는 이유를 이번에 겪었다.
kebab-parse-code가 먼저 실패해서 뒤 크레이트가 아예 컴파일되지 않았고, 그 그늘에 PR #235 에서 내가 넣은unnested_or_patterns위반이 숨어 있었다. 게이트가 붉으면 새 위반이 안 보인다. 셋 다 여기서 고친다.검증
문서 30건을 색인하고 21건을 지운 뒤 재색인:
--json(실제 출력):ndjson 로그 (이슈가 보고한 0바이트가 아니다):
cargo test --workspace --no-fail-fast녹색 (187 ok / 0 FAILED)cargo clippy --workspace --all-targets -- -D warnings녹색 — 이 세션 들어 처음이다시험 항목 (Test Plan)
crates/kebab-app/tests/sweep_progress.rs(신규):sweep_emits_a_bounded_phase_with_one_event_per_candidate— 분모가 후보 수와 맞는가, 진행 이벤트의idx가 1..=total 로 빠짐없이 연속인가, 순서가SweepStarted<SweepProgress*<SweepCompleted인가. 진행바는 길이를 먼저 알아야 위치를 그릴 수 있다.a_sweep_that_purges_nothing_still_announces_itself—include를 좁혀 후보는 있는데 purge 는 0인 경우. "checked 12115, purged 0" 이 사용자가 못 듣고 있던 바로 그 답이므로, 0-purge sweep 도 구간을 알려야 한다.crates/kebab-app/tests/ingest_log_smoke.rs:ingest_log_records_the_deleted_file_sweep— 로그에purge줄이 어느 문서였는지와 함께 남는가,sweep_summary가checked/purged/ms를 들고 있는가.ms는 이 구간이 run 의 병목이었는지를 사용자가 판단하는 근거다.ingest_log_smoke의valid_kinds화이트리스트에 새 두 kind 추가.비범위
reset --orphans-only— 이슈가 참고로 적은 같은 구조의 루프다. reset 에는 진행 채널 자체가 없어서 sweep 하나를 위해 배선을 새로 깔아야 하는데, #229 와 #230 이 머지된 지금 이 경로의 문서당 비용이 약 800배 떨어져 "몇 시간 무표시" 상황이 애초에 안 나온다. 필요해지면 별 건으로 다룬다.버전 영향
wire schema 의 additive minor 변경(신규 이벤트 kind, 기존 소비자 무영향)이라 CLAUDE.md §Versioning cascade 기준 major bump 대상이 아니다. 새 서브커맨드·플래그·config 키도 없고 검색·색인 결과도 그대로다 — 관측성 개선이므로 patch 쪽이다.
Assisted-by: Claude Code
`sweep_deleted_files` 는 walker 가 끝난 직후 asset 루프 전에 도는데, 이 구간이 관측 가능한 신호를 하나도 내지 않았다. 진행바는 walker 총계 (`0/12115`)를 표시한 채 멈춰 보이고 ndjson 로그는 0바이트로 남는다. `tracing::info!` 은 나가지만 wire 이벤트가 아니라 사용자가 볼 산출물이 없다. 프로세스는 CPU 100% 로 정상 동작 중인데 밖에서는 hang 과 구별할 수 없고, 실제 도그푸딩에서 세 번 연속 Ctrl-C 로 죽였다. `ingest_progress.v1` 에 세 이벤트를 추가한다 (additive — 기존 소비자는 모르는 kind 를 무시한다). sweep_started { total } sweep_progress { idx, total, path, removed } sweep_completed { checked, purged, ms } `total` 은 `all_workspace_paths()` 에서 이번 스캔이 덮은 경로를 뺀 값이라 루프 진입 전에 확정 분모가 나온다 (이슈 제안 1). `removed` 는 "정말 없어서 문서를 지웠다" 와 "아직 디스크에 있어 그대로 뒀다" 를 가른다 — 수천 건을 훑고 하나도 안 지우는 sweep 이 있으므로, 지울 때만 움직이는 진행바는 바로 그 경우에 다시 멈춰 보인다. `purged` 를 두 이벤트에서 다른 타입으로 쓰지 않으려고 sweep_progress 쪽은 `removed`(bool), sweep_completed 쪽은 `purged`(정수) 로 이름을 나눴다. 같은 wire 키가 두 타입을 갖는 건 소비자 입장에서 함정이다. ndjson 로그에는 `purge { ts, doc_path }` 와 `sweep_summary { ts, checked, purged, ms }` 를 추가한다 (이슈 제안 2 가 요청한 형태). 로그가 유일한 사후 기록이다 — tracing 은 stderr 로 흘러가고 남지 않는다. CLI 는 sweep 을 asset 진행바와 별 phase 로 그린다. 후보 12k 를 훑는 일과 asset 12k 를 색인하는 일은 분모가 다른 별개의 작업이라 카운터를 공유하면 두 번째 구간이 처음부터 다시 시작하는 것처럼 보인다. 비-TTY 는 실제로 지운 것만 줄로 찍는다 — 검사한 후보마다 한 줄이면 그대로 둔 경로들이 run 의 진짜 출력을 덮는다. 실측 (문서 30건 색인 → 21건 삭제 → 재색인): ingest: sweeping 21 deleted-file candidates… purged doc11.md … (21줄) ingest: sweep complete (checked=21 purged=21 in 134ms) `--json` 은 세 이벤트를 ingest_progress.v1 로 내보내고, ndjson 로그에는 purge 21줄 + sweep_summary 1줄이 남는다. 이슈가 참고로 적은 `reset --orphans-only` 는 그대로 뒀다. reset 에는 진행 채널 자체가 없어 sweep 하나를 위해 배선을 새로 깔아야 하는데, #229 와 #230 이 머지된 지금 이 경로의 문서당 비용이 약 800배 떨어져 "몇 시간 무표시" 상황이 애초에 안 나온다. 곁다리 — clippy 게이트가 붉었다: `cargo clippy --workspace --all-targets -- -D warnings` 가 main 에서 실패하고 있었다. 툴체인이 올라가면서 새 lint 둘(`question_mark`, `manual_assert_eq`)이 기존 코드에 걸린 것이고 각각 한 줄이다. 방치하면 안 되는 이유를 이번에 겪었다 — `kebab-parse-code` 가 먼저 실패해 뒤 크레이트가 아예 컴파일되지 않았고, 그 그늘에 PR #235 에서 내가 넣은 `unnested_or_patterns` 위반이 숨어 있었다. 셋 다 여기서 고친다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF리뷰 두 건에서 나온 지적을 반영한다. 1) sweep 이 끝나도 asset 진행바가 복구되지 않았다 (HIGH) sweep 은 asset 진행바를 빌려 쓰면서 자기 라벨과 자기(더 작은) 총계를 씌운다. 그런데 `AssetStarted` 는 위치와 메시지만 세팅하고 길이·스타일은 건드리지 않는다. 그래서 sweep 이 한 번 돌면 그 뒤 색인 구간 전체가 `sweep [====] 4213/21` 로 그려졌다 — 라벨도 분모도 틀린다. 더 나쁜 건 스타일 교체가 v0.26.1 의 커스텀 키 `{asset_elapsed}` 를 같이 날린다는 점이다. 느린 asset 에서 `(Ns)` 가 도는 게 "멈춘 게 아님"의 유일한 신호인데 sweep 이 그걸 없앤다. 이 PR 이 sweep 구간에서 없앤 "hang 처럼 보임" 을 asset 구간에 새로 만드는 셈이었다. 바 세팅을 `dress_bar_for_assets` 로 빼고 `ScanCompleted` 와 `SweepCompleted` 양쪽에서 부른다. 스타일을 길이보다 먼저 세팅하는데, indicatif 가 두 호출 사이에 다시 그릴 수 있어 과도기 프레임이 최소한 올바른 라벨을 달게 하기 위해서다. TTY 전용이라 비-TTY 실측만 보고 있어서 놓쳤다. pty 로 재현해 고친 뒤 `ingest [====] 16/17 doc9.md` 로 나오는 것을 확인했다. 2) docs/DOGFOOD.md §1.8 이 갱신되지 않았다 (MEDIUM) `--json` 이벤트 순서 목록에 sweep 3종이 빠져 있었다. verify 항목에 sweep 분모·연속성과 "sweep 뒤 진행바가 asset 분모로 돌아오는가" 를 넣었다 — 1번이 정확히 그 항목이 있었으면 잡혔을 결함이다. 3) ingest_progress.rs 의 ordering invariant 주석이 stale 했다 (MEDIUM) §2.4a 순서 블록에 sweep 이 없었다. 설계 문서 자체는 frozen baseline 이고 HOTFIXES dated entry 가 있으니 규약상 문제없지만 코드 주석은 living 이다. 4) 잔가지 (LOW) - purge 실패가 "디스크에 남아 있어 그냥 뒀다" 와 구분되지 않았다. 둘 다 `removed: false` 이고 ndjson 에는 아무것도 안 남아, `sweep_summary` 의 `checked - purged` 차이로도 못 가른다. 사후 기록이 로그뿐이라는 게 이 PR 의 전제이므로 `purge_failed` 를 추가했다. - `SweepProgress` 가 purge 할 때만 바 메시지를 세팅하고 비우지 않아, 마지막 purge 경로가 이후 후보를 훑는 내내 남았다. 안 지울 때는 비운다. - 주석과 스키마가 신규 이벤트를 `v0.33.0` 이라고 적었는데, CLAUDE.md 의 bump 규칙상 이 변경은 patch 다 (additive-only wire + 관측성 개선, 새 명령·플래그·config 없음, 검색·색인 결과 불변 — 선례가 asset_phase). `v0.32.1` 로 고쳤다. 미반영: `SweepCompleted` 가 TTY 에서 `bar.println` 대신 stderr 에 직접 쓰는 것 — 기존 `AssetTimings`/`PdfOcr*` 이 같은 패턴이라 신규 회귀가 아니고, 바꾸려면 그 셋을 같이 옮겨야 해서 별 건이다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF3회차 리뷰가 취소 처리에 CRITICAL/HIGH 가 없음을 확인하고(특히 break 후에도 `flush_vector_deletes` 가 돌아 고아 벡터가 안 생기는 것, `examined` 가 네 경계에서 모두 맞는 것) 머지 가능으로 결론냈다. 남은 지적 다섯을 반영한다. 1) 빈 스캔 + sweep 취소가 Aborted 가 아니라 Completed 로 보고됐다 (MEDIUM) `was_cancelled` 는 asset 루프 **본문**에서만 세팅되는데, 스캔이 0건이면 본문이 한 번도 안 돈다. 하필 그게 sweep 이 가장 커지는 경우다 — 스캔이 0건이면 저장된 모든 경로가 후보이므로, "색인된 디렉토리를 통째로 지우고 재색인 → 긴 sweep → Ctrl-C" 가 정확히 이 구멍에 들어간다. 사용자는 취소했는데 "완료"를 본다. 플래그로 시드한다. `cancelling_a_sweep_with_nothing_left_to_scan_still_ reports_aborted` 로 고정했는데, 처음 쓴 버전은 워크스페이스 상위만 지워서 픽스처가 하위 디렉토리에 남았고 asset 루프가 한 번 돌아 **되돌려도 통과했다**. 재귀 삭제로 고치고, 전제(`ScanCompleted { total: 0 }`)를 어서션으로 박았다. 시드를 되돌리면 실패하는 것을 확인했다. 2) CLI 테스트의 position 어서션에 탐지력이 없었다 (LOW) 두 시나리오를 따로 돌려서 두 번째에 `SweepProgress` 가 없었고, position 은 `SweepStarted` 의 `set_position(0)` 이후 계속 0 이었다. 즉 `dress_bar_for_assets` 안의 `set_position(0)` 을 지워도 통과했다. 하나로 합쳐 sweep 이 position 을 3 까지 올린 뒤 복구를 본다. 3) 그 테스트 주석이 실제 범위보다 넓게 주장했다 (LOW) "하트비트 키까지 고정한다" 고 적었는데 실제로는 length/position 만 본다. indicatif 가 스타일을 되읽을 방법을 주지 않으므로, 그 절반은 `dress_bar_for_assets` 가 양쪽 phase 의 유일한 옷 입히는 자리라는 사실에 기댄다 — 주석을 그렇게 고쳐 적었다. 4) 취소 계약 독 주석이 stale 했다 (LOW) `ingest_with_config` 의 §10 계약에 "in-flight asset 이 끝나고 이후는 스킵" 만 있고 sweep 이 이제 취소를 본다는 사실이 없었다. 5) DOGFOOD §1.8 이 자기 모순이었다 (LOW) `sweep_completed.checked == total` 을 단정하고 바로 다음 줄에서 취소 시엔 다르다고 했다. 앞줄에 "취소 없이 완주하면" 을 붙였다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF