review(회차1): cancel_tx slot 제거 — dead-channel shim 회피

회차 1 의 설계 결함 지적 반영. 원래 IngestState 에 cancel_tx:
Sender<()> 만 두고 receiver 는 start_ingest 안에서 즉시 drop —
실제 send() 호출 시 항상 Err(SendError) 인 dead channel 이 됨.
\"slot 만 정의\" 의도였으나 실용 가치 0 + CLAUDE.md 의 backward-
compat shim 금지 룰 위반.

수정:
- IngestState 에서 cancel_tx field 제거.
- start_ingest 의 cancel channel allocation 제거.
- doc comment 갱신 — p9-fb-04 가 (cancel_tx, cancel_rx) pair 동시
  추가 + receiver 를 worker thread 로 move 하는 형태로 reshape 한다고
  명시.
- test fresh_state helper 도 cancel_tx 인자 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 20:46:10 +00:00
parent 474b776c09
commit 62929efdbd
2 changed files with 13 additions and 17 deletions

View File

@@ -177,8 +177,14 @@ impl Default for InspectState {
/// the final aggregate counts stay on screen for a few seconds and
/// then the slot clears.
///
/// `cancel_tx` is reserved for `p9-fb-04` (Ctrl-C / Esc cancel
/// wiring); this task allocates the channel but never sends on it.
/// `p9-fb-04` adds the cancel surface — at that point this struct
/// gains a real `(cancel_tx, cancel_rx)` pair (the receiver moved
/// into the worker thread alongside the progress sender). We do
/// NOT pre-define a `cancel_tx` slot here because doing so without
/// a matching receiver-bound worker would yield a dead channel
/// (`send` returning `Err(SendError)` forever) — empty slot that
/// pretends to be a future-compat shim is worse than no slot
/// (CLAUDE.md "backward-compat shim 금지").
pub struct IngestState {
pub rx: std::sync::mpsc::Receiver<kebab_app::IngestEvent>,
pub counts: kebab_app::AggregateCounts,
@@ -195,10 +201,6 @@ pub struct IngestState {
/// Worker thread handle. `take()`n at clear time so the join
/// happens after the user has had time to read the final line.
pub thread: Option<std::thread::JoinHandle<anyhow::Result<kebab_core::IngestReport>>>,
/// Cancel-token slot for `p9-fb-04`. Defined here so that task
/// can wire `Esc` / `Ctrl-C` without restructuring `IngestState`.
/// This task never sends on it.
pub cancel_tx: std::sync::mpsc::Sender<()>,
}
/// Seconds the final ingest status line stays on screen after a run

View File

@@ -10,9 +10,11 @@
//! seconds (`TERMINAL_LINE_HOLD_SECS`) and then `tick_clear` returns
//! true so the run loop can drop the slot.
//!
//! `cancel_tx` is allocated here but never sent on — the cancel
//! wiring (`Esc` / `Ctrl-C`) lands in `p9-fb-04`. The slot exists
//! today so this task does not have to reshape `IngestState` later.
//! Cancel surface (Esc / Ctrl-C) lands in `p9-fb-04`; that task
//! adds a `(cancel_tx, cancel_rx)` pair to `IngestState` and
//! threads the receiver through `kebab_app::ingest_with_config_cancellable`.
//! This task does NOT pre-allocate the channel — see the comment on
//! `IngestState` for the rationale.
use std::sync::mpsc;
use std::thread;
@@ -37,11 +39,6 @@ pub fn start_ingest(app: &mut App) -> anyhow::Result<()> {
exclude: cfg.workspace.exclude.clone(),
};
let (tx, rx) = mpsc::channel::<IngestEvent>();
let (cancel_tx, _cancel_rx) = mpsc::channel::<()>();
// _cancel_rx is intentionally dropped here; p9-fb-04 will wire it
// through to `ingest_with_config_cancellable` (or equivalent).
// Holding both ends today keeps the channel allocated without
// doing anything with it.
let cfg_for_thread = cfg;
let thread = thread::spawn(move || {
kebab_app::ingest_with_config_progress(cfg_for_thread, scope, true, Some(tx))
@@ -55,7 +52,6 @@ pub fn start_ingest(app: &mut App) -> anyhow::Result<()> {
terminal_at: None,
aborted: false,
thread: Some(thread),
cancel_tx,
});
Ok(())
}
@@ -196,7 +192,6 @@ mod tests {
fn fresh_state() -> IngestState {
let (_tx, rx) = mpsc::channel::<IngestEvent>();
let (cancel_tx, _cancel_rx) = mpsc::channel::<()>();
IngestState {
rx,
counts: AggregateCounts::default(),
@@ -206,7 +201,6 @@ mod tests {
terminal_at: None,
aborted: false,
thread: None,
cancel_tx,
}
}