feat(cli): kebab reset (p9-fb-06) #49

Merged
altair823 merged 7 commits from feat/p9-fb-06-reset into main 2026-05-02 18:45:25 +00:00
Owner

Summary

kebab reset [--all | --data-only | --vector-only | --config-only] [--yes] 추가. 도그푸딩 후 사용자가 여러 config / 모델 조합 테스트 시 4 개 XDG 경로를 수동 rm -rf 해야 했던 막힘 (도그푸딩 강도 1위) 을 단일 명령 + TTY confirm 으로 해소.

  • TTY confirm prompt (stdin non-interactive 인데 --yes 없으면 exit 2 + hint).
  • --vector-only 가 SQLite embedding_records 도 함께 truncate — off-disk Lance dir 만 wipe 했을 때 store 가 사라진 row 를 가리키는 orphan 상태 회피.
  • kebab init 자동 호출은 안 함 — --all / --config-only 후에는 hint 출력.
  • --json 모드는 frozen reset_report.v1 스키마 (schema_version / scope / removed_paths / embedding_rows_truncated).
  • 새 dep 0개. 자체 20-line confirm_destructive helper (std::io::IsTerminal).

Scope (p9-fb-06)

  • spec: tasks/p9/p9-fb-06-data-reset-command.md
  • plan: docs/superpowers/plans/2026-05-02-p9-fb-06-reset-command.md
  • 도그푸딩 피드백 origin: tasks/p9/p9-dogfooding-feedback.md 항목 4

변경 사항

  • crates/kebab-store-sqlite: truncate_embedding_records() helper + 통합 테스트.
  • crates/kebab-app: reset 모듈 (ResetScope, ResetReport, enumerate_paths, estimate_size_bytes, execute).
  • crates/kebab-cli: Cmd::Reset clap variant (mutually-exclusive scope flags via group = "reset_scope") + handler + confirm_destructive helper + 4 통합 테스트.
  • crates/kebab-cli/src/wire.rs: wire_reset helper.
  • docs/wire-schema/v1/reset_report.schema.json: 새 schema.
  • README.md, HANDOFF.md: 명령 표 + cleanup 절 + dated entry. ARCHITECTURE.md 는 무영향 (crate graph / locked-in decision 변경 없음).

Plan deviation (회차 1 sweep 에서 짚었으나 자기-fix)

  • truncate_embedding_records 가 원래 Result<()> 였는데 Result<u64> 로 bump (DELETE rowcount 직접 리턴 — 별도 COUNT 우회).
  • kebab-app::reset::truncate_embeddingsSqliteStore::open(path) 가정이었는데 실제는 &Config. plan 의 caveat 문구 그대로 fix.
  • 통합 테스트의 stub config.toml = "schema_version = 1" 가 Config parser 의 필수 section 검사에 걸려 marker 파일로 대체. Config::load(None) 의 "파일 없으면 defaults" fallback 활용.

Test plan

  • cargo test -p kebab-store-sqlite --test truncate_embeddings (2 PASS)
  • cargo test -p kebab-app --lib reset (5 PASS)
  • cargo test -p kebab-cli (전체 — wire 6 + reset_cli 4 PASS, 다른 통합 테스트 회귀 0)
  • cargo clippy -p kebab-store-sqlite -p kebab-app -p kebab-cli --all-targets -- -D warnings clean

후속

  • 머지 후 tasks/p9/p9-fb-06-data-reset-command.md frontmatter 를 in_progresscompleted 로 한 줄 commit.
  • 우선순위 다음 후속: p9-fb-01 / 02 / 03 (ingest progress) 묶음.
## Summary `kebab reset [--all | --data-only | --vector-only | --config-only] [--yes]` 추가. 도그푸딩 후 사용자가 여러 config / 모델 조합 테스트 시 4 개 XDG 경로를 수동 `rm -rf` 해야 했던 막힘 (도그푸딩 강도 1위) 을 단일 명령 + TTY confirm 으로 해소. - TTY confirm prompt (stdin non-interactive 인데 `--yes` 없으면 exit 2 + hint). - `--vector-only` 가 SQLite `embedding_records` 도 함께 truncate — off-disk Lance dir 만 wipe 했을 때 store 가 사라진 row 를 가리키는 orphan 상태 회피. - `kebab init` 자동 호출은 안 함 — `--all` / `--config-only` 후에는 hint 출력. - `--json` 모드는 frozen `reset_report.v1` 스키마 (`schema_version` / `scope` / `removed_paths` / `embedding_rows_truncated`). - 새 dep 0개. 자체 20-line `confirm_destructive` helper (`std::io::IsTerminal`). ## Scope (p9-fb-06) - spec: `tasks/p9/p9-fb-06-data-reset-command.md` - plan: `docs/superpowers/plans/2026-05-02-p9-fb-06-reset-command.md` - 도그푸딩 피드백 origin: `tasks/p9/p9-dogfooding-feedback.md` 항목 4 ## 변경 사항 - `crates/kebab-store-sqlite`: `truncate_embedding_records()` helper + 통합 테스트. - `crates/kebab-app`: `reset` 모듈 (`ResetScope`, `ResetReport`, `enumerate_paths`, `estimate_size_bytes`, `execute`). - `crates/kebab-cli`: `Cmd::Reset` clap variant (mutually-exclusive scope flags via `group = "reset_scope"`) + handler + `confirm_destructive` helper + 4 통합 테스트. - `crates/kebab-cli/src/wire.rs`: `wire_reset` helper. - `docs/wire-schema/v1/reset_report.schema.json`: 새 schema. - `README.md`, `HANDOFF.md`: 명령 표 + cleanup 절 + dated entry. ARCHITECTURE.md 는 무영향 (crate graph / locked-in decision 변경 없음). ## Plan deviation (회차 1 sweep 에서 짚었으나 자기-fix) - `truncate_embedding_records` 가 원래 `Result<()>` 였는데 `Result<u64>` 로 bump (DELETE rowcount 직접 리턴 — 별도 COUNT 우회). - `kebab-app::reset::truncate_embeddings` 가 `SqliteStore::open(path)` 가정이었는데 실제는 `&Config`. plan 의 caveat 문구 그대로 fix. - 통합 테스트의 stub `config.toml = "schema_version = 1"` 가 Config parser 의 필수 section 검사에 걸려 marker 파일로 대체. `Config::load(None)` 의 \"파일 없으면 defaults\" fallback 활용. ## Test plan - [x] `cargo test -p kebab-store-sqlite --test truncate_embeddings` (2 PASS) - [x] `cargo test -p kebab-app --lib reset` (5 PASS) - [x] `cargo test -p kebab-cli` (전체 — wire 6 + reset_cli 4 PASS, 다른 통합 테스트 회귀 0) - [x] `cargo clippy -p kebab-store-sqlite -p kebab-app -p kebab-cli --all-targets -- -D warnings` clean ## 후속 - 머지 후 `tasks/p9/p9-fb-06-data-reset-command.md` frontmatter 를 `in_progress` → `completed` 로 한 줄 commit. - 우선순위 다음 후속: p9-fb-01 / 02 / 03 (ingest progress) 묶음.
altair823 added 6 commits 2026-05-02 18:39:49 +00:00
Wipes every row from embedding_records and returns the deleted row
count. Used by the upcoming `kebab reset --vector-only` to keep SQLite
consistent after the on-disk Lance store is removed.

Plan deviation from the original spec (task 1):
- Original test plan opened SqliteStore with a raw path; the actual
  signature is `SqliteStore::open(&Config)`, so the integration test
  builds a Config with `storage.data_dir` pointed at a tempdir.
- Original return type was Result<()>; bumped to Result<u64> so the
  caller (kebab-app::reset) can surface the truncated count in the
  reset_report.v1 wire payload without a separate COUNT query.

p9-fb-06 task 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Provides the wipe core for `kebab reset`. Mutually-exclusive ResetScope
variants (All / DataOnly / VectorOnly / ConfigOnly), pure path
enumeration for the confirm UI preview, byte-size estimator, and an
execute helper that removes paths off-disk + truncates
embedding_records when scope is VectorOnly.

Plan deviation from the original spec (task 2):
- Original `truncate_embeddings` helper opened SqliteStore via path and
  ran a separate COUNT query through `lock_conn` (private). Both APIs
  are unavailable from outside the crate, so the helper now opens the
  store via `SqliteStore::open(&Config)` and lets
  `truncate_embedding_records` (task 1) report the deleted count
  directly.
- Skipped the XDG-env-overriding unit test from the original plan to
  avoid race conditions with sibling tests; the equivalent integration
  coverage moves up to the CLI tests in task 4 where each invocation
  runs in a fresh process.
- Added an FS-touching unit test (`estimate_size_sums_file_lengths`)
  to cover the read-side of `estimate_size_bytes` against a tempdir.

p9-fb-06 task 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSON Schema 7 frozen surface for `kebab reset --json`. Mirrors the
ResetReport struct from kebab-app. Test asserts schema_version tag,
scope serialization (snake_case enum), removed_paths array, and
embedding_rows_truncated u64.

p9-fb-06 task 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mutually-exclusive scope flags (--all / --data-only / --vector-only /
--config-only via clap ArgGroup) plus --yes for non-interactive use.
Aborts with exit 2 when stdin is non-interactive and --yes is missing
— silent destruction is forbidden. Self-contained 20-line confirm
prompt (no new dep; std::io::IsTerminal).

Integration tests exercise the bin in a fresh subprocess against
tempdir-rooted XDG env to keep the assertions independent of the host
config:
- --data-only --yes wipes data + cache + state, preserves config.
- non-TTY without --yes exits 2 with the documented hint.
- --json emits reset_report.v1 schema with snake_case scope.
- conflicting --all + --data-only rejected by clap before any wipe.

Plan deviation (task 4): the data-only test used to write a stub
config.toml containing only `schema_version = 1`, but Config parsing
requires every section. Switched to a marker file in the cfg dir +
the documented Config::load(None)→defaults fallback.

p9-fb-06 task 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3-doc sync rule (CLAUDE.md): user-visible CLI surface change → README
and HANDOFF land in the same PR. ARCHITECTURE.md is not touched —
kebab reset doesn't move the crate graph or any locked-in technical
decision.

p9-fb-06 task 5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final flip to completed lands in a separate one-line commit AFTER PR
merges so spec history reflects reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 requested changes 2026-05-02 18:42:09 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 1 — critical 1건 + nit 1건 + 칭찬 3건.

핵심 actionable:

  1. (critical) embeddings.rs 의 truncate_embedding_records 위치 — mark_embedding_records_committed 의 doc comment 14 줄을 흡수. Rust 의 /// 가 다음 항목에 attach 되는 규칙 때문에 위쪽 mark_committed 함수의 doc 이 truncate 함수로 옮겨지고 mark_committed 는 doc 없이 남음. truncate 를 impl block 끝으로 옮기면 plan 의 원래 의도와도 일치 + mark_committed 의 design rationale (WHERE status='pending' 이유 등) 보존.
  2. (nit) integration test 의 removed_paths 길이 assert 보강 — idempotency 약속 ("존재하지 않는 path 는 omit") 묶어 검증.

전체 인상: store/app/cli/wire/test/docs 6 commit 이 명확히 layer 별 분리. plan deviation 3건 모두 commit 메시지에 명시. clap ArgGroup 으로 mutually-exclusive scope 강제 + integration test 로 edge case 4 종 cover. 위 1건 정리하면 머지 가능.

회차 1 — critical 1건 + nit 1건 + 칭찬 3건. 핵심 actionable: 1. **(critical) `embeddings.rs` 의 truncate_embedding_records 위치 — `mark_embedding_records_committed` 의 doc comment 14 줄을 흡수.** Rust 의 `///` 가 다음 항목에 attach 되는 규칙 때문에 위쪽 mark_committed 함수의 doc 이 truncate 함수로 옮겨지고 mark_committed 는 doc 없이 남음. truncate 를 impl block 끝으로 옮기면 plan 의 원래 의도와도 일치 + mark_committed 의 design rationale (`WHERE status='pending'` 이유 등) 보존. 2. **(nit)** integration test 의 `removed_paths` 길이 assert 보강 — idempotency 약속 (\"존재하지 않는 path 는 omit\") 묶어 검증. 전체 인상: store/app/cli/wire/test/docs 6 commit 이 명확히 layer 별 분리. plan deviation 3건 모두 commit 메시지에 명시. clap ArgGroup 으로 mutually-exclusive scope 강제 + integration test 로 edge case 4 종 cover. 위 1건 정리하면 머지 가능.

(칭찬) data_only: _, pattern binding + // The data_only: _ binding above is intentional ... 코멘트가 미래 reader 의 "왜 unused 인가" 의문을 코드 옆에서 즉시 차단. clap group = "reset_scope" 의 mutual exclusion 보장이 코드 옆 한 줄로 명시되어, data_only 자체가 resolution 에 영향 없는 이유까지 설명.

(칭찬) `data_only: _,` pattern binding + `// The data_only: _ binding above is intentional ...` 코멘트가 미래 reader 의 "왜 unused 인가" 의문을 코드 옆에서 즉시 차단. clap `group = "reset_scope"` 의 mutual exclusion 보장이 코드 옆 한 줄로 명시되어, `data_only` 자체가 resolution 에 영향 없는 이유까지 설명.

(칭찬) Non-TTY + --yes 부재 케이스를 anyhow::bail! 로 처리해 자동으로 exit 2 (default error path) 로 빠지면서 eprintln! + 메시지 한 번에 — silent destruction 금지 가드를 minimum surface 로 구현. integration test reset_no_yes_in_non_tty_aborts_with_exit_2 가 정확히 이 경로 검증.

(칭찬) Non-TTY + `--yes` 부재 케이스를 `anyhow::bail!` 로 처리해 자동으로 exit 2 (default error path) 로 빠지면서 `eprintln!` + 메시지 한 번에 — silent destruction 금지 가드를 minimum surface 로 구현. integration test `reset_no_yes_in_non_tty_aborts_with_exit_2` 가 정확히 이 경로 검증.

(보강 권장) data_only 가 data + cache + state 3 dir 모두 wipe 하는데 test 가 그 셋 모두 not exists 검증 — 좋음. 다만 removed_paths 의 길이 (Vec<PathBuf>) 도 함께 assert 하면 execute 가 "존재하지 않는 path 는 omit" 한다는 idempotency 약속을 묶어서 검증 가능.

Why: 현재 --json 테스트는 removed_paths 가 array 인 것만 확인. data 만 만들고 cache/state 는 만들지 않은 케이스가 있으면 length 가 1 임을 명시 검증.

How to apply: reset_data_only_yes_json_emits_reset_report_v1assert_eq!(v.get("removed_paths").and_then(|a| a.as_array()).map(|a| a.len()), Some(1)); 한 줄 추가 — data 만 만들고 cache/state 는 안 만들었으니 길이=1. 이번 PR 안에서 해소 권장 (작은 변경).

(보강 권장) `data_only` 가 data + cache + state 3 dir 모두 wipe 하는데 test 가 그 셋 모두 not exists 검증 — 좋음. 다만 `removed_paths` 의 길이 (`Vec<PathBuf>`) 도 함께 assert 하면 `execute` 가 "존재하지 않는 path 는 omit" 한다는 idempotency 약속을 묶어서 검증 가능. Why: 현재 `--json` 테스트는 `removed_paths` 가 array 인 것만 확인. data 만 만들고 cache/state 는 만들지 않은 케이스가 있으면 length 가 1 임을 명시 검증. How to apply: `reset_data_only_yes_json_emits_reset_report_v1` 에 `assert_eq!(v.get("removed_paths").and_then(|a| a.as_array()).map(|a| a.len()), Some(1));` 한 줄 추가 — data 만 만들고 cache/state 는 안 만들었으니 길이=1. 이번 PR 안에서 해소 권장 (작은 변경).
@@ -107,6 +107,22 @@ impl SqliteStore {
/// WHERE embedding_id IN (?, ?, …)`) inside one transaction —
/// avoids the per-row `execute()` round-trip the previous
/// implementation paid.
/// Wipe every row from `embedding_records`, returning the count of

(critical / doc 흡수 버그) truncate_embedding_records 의 doc comment 가 mark_embedding_records_committed 함수의 doc comment 직후에 인접 — Rust 의 /// 는 다음 항목에 attach 되므로 위쪽 14 줄짜리 mark_committed doc 이 truncate 의 doc 으로 흡수되고, mark_committed 자체는 doc 없이 pub fn 만 남음.

Why: 이 함수의 doc 은 phase-3 commit 의 "왜 WHERE status='pending' 만 대상인가 / 왜 단일 transaction 인가" 같은 design rationale 을 담고 있어서 잃어버리면 미래 reader 가 WHERE status='pending' 의 의도 (tombstone 보존, 중복 batch 방지) 를 추적 불가.

How to apply: truncate_embedding_recordsimpl SqliteStore 블록 (mark_committed 의 닫는 } 다음) 으로 옮긴다. plan 의 원래 의도 ("Append at the end of impl SqliteStore") 와도 일치. 옮긴 후 mark_committed 의 doc comment 14 줄이 다시 그 함수에 attach 되는지 확인.

(critical / doc 흡수 버그) `truncate_embedding_records` 의 doc comment 가 `mark_embedding_records_committed` 함수의 doc comment 직후에 인접 — Rust 의 `///` 는 다음 항목에 attach 되므로 위쪽 14 줄짜리 mark_committed doc 이 truncate 의 doc 으로 흡수되고, mark_committed 자체는 doc 없이 `pub fn` 만 남음. Why: 이 함수의 doc 은 phase-3 commit 의 "왜 WHERE status='pending' 만 대상인가 / 왜 단일 transaction 인가" 같은 design rationale 을 담고 있어서 잃어버리면 미래 reader 가 `WHERE status='pending'` 의 의도 (tombstone 보존, 중복 batch 방지) 를 추적 불가. How to apply: `truncate_embedding_records` 를 `impl SqliteStore` 블록 **끝** (mark_committed 의 닫는 `}` 다음) 으로 옮긴다. plan 의 원래 의도 ("Append at the end of `impl SqliteStore`") 와도 일치. 옮긴 후 mark_committed 의 doc comment 14 줄이 다시 그 함수에 attach 되는지 확인.
@@ -0,0 +6,4 @@
"type": "object",
"required": [
"schema_version",
"scope",

(칭찬) 기존 sibling schema 들 (ingest_report.v1, doctor.v1 등) 의 $id URL pattern (https://kb.local/wire/v1/...) 을 정확히 따르고, additionalProperties 필드를 명시 안 한 것까지 sibling 일관 (sibling 들도 명시 안 함). frozen wire schema 의 가족 일원이 됨.

(칭찬) 기존 sibling schema 들 (`ingest_report.v1`, `doctor.v1` 등) 의 `$id` URL pattern (`https://kb.local/wire/v1/...`) 을 정확히 따르고, `additionalProperties` 필드를 명시 안 한 것까지 sibling 일관 (sibling 들도 명시 안 함). frozen wire schema 의 가족 일원이 됨.
altair823 added 1 commit 2026-05-02 18:43:29 +00:00
- (critical) embeddings.rs: truncate_embedding_records 위치 이동.
  mark_embedding_records_committed 함수 위에 끼워 넣었더니 위쪽
  mark_committed 의 14 줄짜리 doc comment (`WHERE status='pending'`
  의 design rationale 등) 가 truncate 의 doc 으로 흡수되고
  mark_committed 자체는 doc 없이 남는 버그. impl block 끝 (mark_committed
  의 닫는 } 다음) 으로 옮겨 plan 의 원래 의도와도 일치.
- (nit) tests/reset_cli.rs: removed_paths 의 idempotency 검증 보강.
  data dir 은 reported, cache dir 은 omit (생성 안 했으니)
  되어야 함을 strict 하게 assert. state dir 은 logging init 의
  side-effect 로 자동 생성되어 둘 다 가능하므로 허용.

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

회차 2 — 회차 1 critical (truncate doc 흡수) + nit (removed_paths idempotency assert) 모두 정확히 반영. 추가 actionable 0. APPROVE.

회차 2 — 회차 1 critical (truncate doc 흡수) + nit (removed_paths idempotency assert) 모두 정확히 반영. 추가 actionable 0. APPROVE.
altair823 merged commit b6203514c5 into main 2026-05-02 18:45:25 +00:00
altair823 deleted branch feat/p9-fb-06-reset 2026-05-02 18:45:26 +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#49