--- title: "v0.20.0 sub-item 1 bugfix round 3 — plan" created: 2026-05-27 status: DRAFT round: 0 spec_path: docs/superpowers/specs/2026-05-27-v0.20-sub1-bugfix3-spec.md parent_spec: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md brief: .omc/reviews/2026-05-27-v0.20-bugfix3-plan-drafter-brief.md branch: feat/pdf-scanned-ocr base_head: f763049 step_count: 7 commit_count: 6 estimated_minutes: 60 --- # v0.20.0 sub-item 1 bugfix round 3 — plan Spec ACCEPT (`2026-05-27-v0.20-sub1-bugfix3-spec.md`, 410 line, 11/11 critic finding 반영) 의 step-level decomposition. 5 bug (#9 / #10 / #11 / #13 / #14) + HOTFIXES + parent spec cross-link 까지 한 round 에서 처리. ## §0 Overview ### §0.1 Scope | Bug | Severity | Surface | Type | |-----|----------|---------|------| | #9 | critical | `schema.v1.capabilities` | wire field correction (false → true) | | #10 | medium | `error.v1` | additive error code `config_not_found` | | #11 | critical UX | `config.pdf.ocr.request_timeout_secs` default | numeric default change (600 → 60) | | #13 | medium | `schema.v1.models` | additive array fields (backward compat) | | #14 | minor UX | `kebab search` / `kebab ask` input validation | additive error code path (`invalid_input`) | | — | — | `tasks/HOTFIXES.md` + parent spec | docs handoff for Bug #11 deviation | ### §0.2 Strategy - **Per-bug commit boundary** (option A): 한 commit 당 한 bug 만 — revert / bisect 가 정확. Step 6 만 doc-only. - **wire schema = additive minor**. Bug #13 `models` 의 신규 두 field 는 optional. 기존 client 영향 0. spec §3.4 와 일치. - **parent spec frozen**: text 변경 0. inline HTML 주석 cross-link 만. HOTFIXES.md 가 live source of truth. - **subagent skip**: in-session direct execution. spec §7 의 worker protocol 준수. - **regression budget**: 기존 workspace test 1350 + 본 round 새 +7 ≥ 1357 test, 모두 green. ### §0.3 Environment ```bash cd /home/altair823/kebab export CARGO_TARGET_DIR=/build/out/cargo-target/target git status # working tree clean expected git rev-parse HEAD # f763049 ``` `-j 4` default (workspace memory budget). `-j 1` 은 OOM fallback only — full workspace integration run 일 때만. --- ## §1 Step table | Step | Subject | Files | New tests | Commit | |------|---------|-------|-----------|--------| | 1 | Bug #9 capabilities flip | `crates/kebab-app/src/schema.rs` | 2 unit | `fix(app): flip streaming_ask + single_file_ingest capabilities to actual surface (Bug #9)` | | 2 | Bug #10 config_not_found error | `crates/kebab-config/src/lib.rs`, `crates/kebab-app/src/error_signal.rs`, `crates/kebab-app/src/error_wire.rs`, `crates/kebab-app/src/lib.rs` (re-export) | 1 unit + 2 integration | `fix(config): emit error.v1 code=config_not_found for missing --config path (Bug #10)` | | 3 | Bug #11 OCR timeout 60s | `crates/kebab-config/src/lib.rs` | 1 unit | `fix(config): pdf.ocr.request_timeout_secs default 600 → 60 per dogfood evidence (Bug #11)` | | 4 | Bug #13 active_parsers + active_chunkers (additive) | `crates/kebab-store-sqlite/src/store.rs` (또는 lib.rs), `crates/kebab-app/src/schema.rs`, `docs/wire-schema/v1/schema.schema.json`, `integrations/claude-code/kebab/SKILL.md` | 2 integration | `feat(schema): add active_parsers + active_chunkers arrays to schema.v1.models (Bug #13)` | | 5 | Bug #14 empty query invalid_input | `crates/kebab-cli/src/main.rs` | 2 integration | `fix(cli): empty query emits error.v1 invalid_input for search + ask (Bug #14)` | | 6 | HOTFIXES + parent spec cross-link | `tasks/HOTFIXES.md`, `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` | 0 | `docs(spec): HOTFIXES entry + parent spec cross-link for Bug #11 timeout deviation` | | 7 | Final sanity (no commit) | — | — | n/a | **Total**: 7 step, 6 commit, 8 new test (3 unit + 5 integration). Reaches spec §5 AC-1 ~ AC-10. --- ## §2 Per-step detail ### Step 1 — Bug #9 capabilities flip #### §2.1.1 Files affected - `crates/kebab-app/src/schema.rs:137-151` (`capabilities_snapshot()`). - `crates/kebab-app/src/schema.rs` (`#[cfg(test)] mod` 또는 새 `mod tests_capabilities` — 기존 `mod tests_stats_ext` 와 동등 위치). #### §2.1.2 Action `capabilities_snapshot()` body 의 두 줄만 변경: ```diff fn capabilities_snapshot() -> Capabilities { Capabilities { json_mode: true, ingest_progress: true, ingest_cancellation: true, rag_multi_turn: true, search_cache: true, incremental_ingest: true, - streaming_ask: false, + streaming_ask: true, http_daemon: false, mcp_server: true, - single_file_ingest: false, + single_file_ingest: true, bulk_search: true, } } ``` `http_daemon: false` 는 보존 — 별도 sub-item 의 non-impl. spec §3.1 의 결정과 일치. #### §2.1.3 New tests `crates/kebab-app/src/schema.rs` 의 `#[cfg(test)]` 영역에 unit test 2 개 추가: ```rust #[test] fn capabilities_streaming_ask_matches_cli_surface() { // Bug #9: kebab ask --stream 가 answer_event.v1 ndjson 정상 emit (191 event 검증) → // capabilities.streaming_ask 가 true 여야 함. let caps = super::capabilities_snapshot(); assert!(caps.streaming_ask, "streaming_ask must be true (Bug #9)"); } #[test] fn capabilities_single_file_ingest_matches_cli_surface() { // Bug #9: kebab ingest-file + kebab ingest-stdin --title 양쪽 모두 // ingest_report.v1 정상 emit → capabilities.single_file_ingest 가 true 여야 함. let caps = super::capabilities_snapshot(); assert!(caps.single_file_ingest, "single_file_ingest must be true (Bug #9)"); } ``` 추가로 `capabilities_snapshot` 가 `pub(crate)` 또는 module-internal 일 경우 `super::` path 로 접근. private 라면 같은 module 의 child mod 에서 호출 가능. #### §2.1.4 Per-step acceptance ```bash cargo test -p kebab-app capabilities_streaming_ask_matches_cli_surface -j 4 cargo test -p kebab-app capabilities_single_file_ingest_matches_cli_surface -j 4 cargo test -p kebab-app schema -j 4 # 기존 schema_report.rs integration 도 green 유지 cargo clippy -p kebab-app --all-targets -- -D warnings ``` 기존 `mod tests_stats_ext` 의 `stats_includes_breakdowns_and_bytes_on_fresh_corpus` (schema_with_config 경유) 가 `streaming_ask`/`single_file_ingest` 를 assert 안 함 — regression 없음. #### §2.1.5 Commit ```bash git add crates/kebab-app/src/schema.rs git commit -m "$(cat <<'EOF' fix(app): flip streaming_ask + single_file_ingest capabilities to actual surface (Bug #9) capabilities_snapshot() 가 streaming_ask + single_file_ingest 를 hardcoded false 로 보고했으나 실제 구현은 v0.20 final-dogfood 에서 production-grade: - kebab ask --stream → answer_event.v1 ndjson 191 event 정상 emit - kebab ingest-file / kebab ingest-stdin --title → ingest_report.v1 정상 MCP host + Claude Code skill 등 agent 가 schema.capabilities 로 routing 결정 시 false negative → 사용자가 실제 동작 feature 를 사용 불가능하다고 오인. http_daemon 은 false 유지 (별도 sub-item 의 non-impl). EOF )" ``` --- ### Step 2 — Bug #10 ConfigNotFound + classify arm #### §2.2.1 Files affected 1. `crates/kebab-config/src/lib.rs` - 19-22 line 의 `ConfigInvalid` 정의 옆에 `ConfigNotFound` 추가. - 688-722 line 의 `Config::load` 안 `Some(_) => Self::defaults(),` arm 을 `Some(_) => Err(...)` 로 변경. 2. `crates/kebab-app/src/error_signal.rs` - `pub use kebab_config::ConfigNotFound;` 추가 (기존 `ConfigInvalid` 와 동등 pattern). 3. `crates/kebab-app/src/error_wire.rs` - `classify` 안 `ConfigInvalid` arm 다음에 `ConfigNotFound` arm 추가. 4. `crates/kebab-app/src/lib.rs` - `pub use kebab_config::ConfigInvalid;` 옆에 `pub use kebab_config::ConfigNotFound;` 추가 (기존 14 line pattern). #### §2.2.2 Action **(a) `crates/kebab-config/src/lib.rs` — error type 추가** (line ~25, `ConfigInvalid` 직후): ```rust /// p20-bugfix3 Bug #10: explicit `--config ` 가 missing 시 silent /// fallback to defaults 대신 fail-fast. `kebab-app::error_wire::classify` /// 가 downcast → `code: "config_not_found"` ErrorV1. #[derive(Debug, thiserror::Error)] #[error("config file does not exist: {path}")] pub struct ConfigNotFound { pub path: PathBuf, } ``` **(b) `crates/kebab-config/src/lib.rs:688-722` — `Config::load` 분기 수정**: ```diff pub fn load(path: Option<&Path>) -> anyhow::Result { let from_disk = match path { Some(p) if p.exists() => Self::from_file(p)?, - Some(_) => Self::defaults(), + Some(p) => { + // Bug #10: explicit --config 가 missing → silent default fallback 금지. + return Err(anyhow::Error::new(ConfigNotFound { + path: p.to_path_buf(), + })); + } None => { let p = Self::xdg_config_path(); ... ``` 상대경로 cover: `Path::exists()` 는 cwd-relative — spec §6 R-1 해결 (별도 작업 0). **(c) `crates/kebab-app/src/error_signal.rs` — re-export**: ```rust pub use kebab_config::ConfigNotFound; ``` (기존 `ConfigInvalid` re-export 와 동등 위치. 같은 file 안에서 `use kebab_config::ConfigInvalid;` 이미 있다면 그 옆.) **(d) `crates/kebab-app/src/error_wire.rs::classify`** — `ConfigInvalid` arm 직후: ```rust if let Some(s) = err.downcast_ref::() { return ErrorV1 { schema_version: ERROR_V1_ID.to_string(), code: "config_not_found".to_string(), message: s.to_string(), details: json!({ "path": s.path.to_string_lossy(), }), hint: Some( "verify --config ; pass an existing toml file or omit --config to use XDG default" .to_string(), ), }; } ``` 상단 `use crate::error_signal::{ConfigInvalid, LlmError, NotIndexed};` 에 `ConfigNotFound` 추가. **(e) `crates/kebab-app/src/lib.rs:14` — public re-export**: ```rust pub use kebab_config::{ConfigInvalid, ConfigNotFound}; ``` #### §2.2.3 New tests **(a) `crates/kebab-config/src/lib.rs` — unit test (기존 `tests` mod 안)**: ```rust #[test] fn config_load_explicit_nonexistent_path_returns_config_not_found() { // Bug #10: --config /tmp/nonexistent.toml → silent fallback 금지. let p = std::path::Path::new("/tmp/__kebab_bugfix3_nonexistent.toml"); assert!(!p.exists(), "test precondition: path must not exist"); let err = Config::load(Some(p)).expect_err("expected ConfigNotFound"); let signal = err .downcast_ref::() .expect("from_load error should downcast to ConfigNotFound"); assert_eq!(signal.path, p.to_path_buf()); } ``` **(b) `crates/kebab-cli/tests/cli_error_wire.rs` 또는 신규 `crates/kebab-cli/tests/cli_config_not_found.rs` — integration test 2 개**: ```rust use std::process::Command; use serde_json::Value; fn kebab_bin() -> String { env!("CARGO_BIN_EXE_kebab").to_string() } #[test] fn invalid_config_path_emits_error_v1_with_nonzero_exit() { let absent = "/tmp/__kebab_bugfix3_absolute_nonexistent.toml"; assert!(!std::path::Path::new(absent).exists()); let out = Command::new(kebab_bin()) .args(["search", "rust", "--config", absent, "--json"]) .output() .expect("spawn kebab"); assert_ne!(out.status.code(), Some(0), "exit must be nonzero on missing --config"); let stderr = String::from_utf8_lossy(&out.stderr); let last_line = stderr.lines().last().expect("error.v1 line on stderr"); let v: Value = serde_json::from_str(last_line) .unwrap_or_else(|e| panic!("expected error.v1 ndjson on stderr: {e}\nstderr={stderr}")); assert_eq!(v["schema_version"], "error.v1"); assert_eq!(v["code"], "config_not_found"); assert!(v["hint"].is_string(), "hint must be present"); } #[test] fn invalid_relative_config_path_emits_config_not_found() { // Bug #10 spec §6 R-1: relative path 도 cwd-relative 로 cover. let tmp = tempfile::tempdir().unwrap(); let out = Command::new(kebab_bin()) .args(["search", "rust", "--config", "nonexistent-rel.toml", "--json"]) .current_dir(tmp.path()) .output() .expect("spawn kebab"); assert_ne!(out.status.code(), Some(0)); let stderr = String::from_utf8_lossy(&out.stderr); let last_line = stderr.lines().last().expect("error.v1 line"); let v: Value = serde_json::from_str(last_line).expect("ndjson"); assert_eq!(v["code"], "config_not_found"); } ``` 기존 `cli_error_wire.rs` 의 ConfigInvalid integration test 패턴을 참고 (existing test 그대로 green 유지 — fail-fast 가 `ConfigInvalid` (file 존재 + parse 실패) 와 별개 path). #### §2.2.4 Per-step acceptance ```bash cargo test -p kebab-config config_load_explicit_nonexistent_path -j 4 cargo test -p kebab-cli invalid_config_path_emits_error_v1_with_nonzero_exit -j 4 cargo test -p kebab-cli invalid_relative_config_path_emits_config_not_found -j 4 cargo test -p kebab-config -j 4 # 기존 18 test 전수 green cargo test -p kebab-app error_wire -j 4 # 기존 classify test 전수 green (ConfigInvalid 등) cargo clippy -p kebab-config -p kebab-app -p kebab-cli --all-targets -- -D warnings ``` `Config::load` 의 `None → XDG default` path 는 변경 0 — `kebab doctor` (config 없는 fresh clone) regression 없음. #### §2.2.5 Commit ```bash git add crates/kebab-config/src/lib.rs \ crates/kebab-app/src/error_signal.rs \ crates/kebab-app/src/error_wire.rs \ crates/kebab-app/src/lib.rs \ crates/kebab-cli/tests/ git commit -m "$(cat <<'EOF' fix(config): emit error.v1 code=config_not_found for missing --config path (Bug #10) 이전: `kebab search "rust" --config /tmp/nonexistent.toml --json` 가 exit=0 + `{"hits":[]}` silent fallback to XDG default. typo / wrong path 가 0-hit 으로만 surface — debugging nightmare. 이후: kebab_config::ConfigNotFound thiserror::Error 추가, Config::load 의 `Some(p) if !p.exists()` arm 이 anyhow::Error::new(ConfigNotFound { path }) return. kebab_app::error_wire::classify 가 downcast → ErrorV1 code=config_not_found, hint, details.path 채워서 stderr 에 ndjson 으로 emit. R-1 (relative path): std::path::Path::exists() 는 cwd-relative — 별도 작업 없이 absolute + relative 모두 cover. integration test 두 개로 검증. EOF )" ``` --- ### Step 3 — Bug #11 OCR timeout 60s #### §2.3.1 Files affected - `crates/kebab-config/src/lib.rs:477` (`default_pdf_ocr_request_timeout_secs`). - `crates/kebab-config/src/lib.rs` `#[cfg(test)] mod tests` (또는 적합한 위치) — 신규 unit test 추가. #### §2.3.2 Action ```diff -fn default_pdf_ocr_request_timeout_secs() -> u64 { 600 } +/// PDF OCR per-page request timeout 의 기본값. +/// 6-32s 가 정상 throughput; 60s 초과는 Ollama 다운 / 매우 dense·고해상도 page 의 신호. +/// `config.toml` 의 `[pdf.ocr] request_timeout_secs = N` 로 override. +/// +/// HOTFIXES 2026-05-27 (Bug #11): metro-korea.pdf dogfood 에서 page 8/13 모두 +/// 기존 600s default 까지 완전 timeout (`chars: 0, skipped: true` × 20분 cost) → +/// 60s 로 하향. parent spec §1000 / §1628 OQ-1 (CPU 환경 105s 의 5x 여유) 가 +/// 가정한 "page 당 평균 105s" 보다 실측 cloud GPU Ollama 가 6-32s 로 훨씬 빠름. +fn default_pdf_ocr_request_timeout_secs() -> u64 { 60 } ``` 기존 470 line 의 `request_timeout_secs: default_pdf_ocr_request_timeout_secs(),` 는 동일 함수 호출이라 추가 변경 0. #### §2.3.3 New tests `crates/kebab-config/src/lib.rs` 의 `#[cfg(test)] mod tests`: ```rust #[test] fn pdf_ocr_request_timeout_default_is_60s() { // Bug #11 (dogfood 2026-05-27): default 600s → 60s. let cfg = PdfOcrCfg::defaults(); assert_eq!( cfg.request_timeout_secs, 60, "pdf.ocr.request_timeout_secs default must be 60s (Bug #11, HOTFIXES 2026-05-27)" ); } ``` 기존 unit test 중 600 magic number 를 검증하는 항목이 있다면 동일 commit 안에서 60 으로 갱신. (verify: `grep -rn "request_timeout_secs.*600\|600.*request_timeout_secs" crates/kebab-config/src/` — 발견 시 그 test 만 expect 값 갱신, 새 unit test 와 같은 의미라면 기존 test 만 갱신하고 신규 test 생략 가능. 본 plan 은 spec ACCEPT 의 보수적 선택: 신규 test 도 추가해 unique name 으로 보존.) #### §2.3.4 Per-step acceptance ```bash cargo test -p kebab-config pdf_ocr_request_timeout_default_is_60s -j 4 cargo test -p kebab-config -j 4 # 18 test 전수 green; 만일 기존 test 가 600 expect 면 같은 commit 에서 갱신 cargo clippy -p kebab-config --all-targets -- -D warnings ``` `PdfOcrCfg::defaults()` 의 다른 field 는 변경 0 — `max_pixels` (2048), `valid_ratio_threshold` (0.5), `min_char_count` (20), `lang_hint` (`"kor"`) 보존. #### §2.3.5 Commit ```bash git add crates/kebab-config/src/lib.rs git commit -m "$(cat <<'EOF' fix(config): pdf.ocr.request_timeout_secs default 600 → 60 per dogfood evidence (Bug #11) metro-korea.pdf v0.20 final-dogfood (2026-05-27): - page 8 + page 13 양쪽 모두 600s default 까지 완전 timeout (`ms: 600000, chars: 0, skipped: true`) - 결과: 본문 indexed 안 됨 + page 당 20분 cost 낭비 cloud GPU Ollama 의 실측 per-page throughput 는 6-32s (parent spec 가정 105s 보다 훨씬 빠름). 60s 면 production-friendly upper-bound. dense/고해상도 page 는 config.toml override (`[pdf.ocr] request_timeout_secs = N`) 로 user 가 늘릴 수 있음 — Step 6 에서 HOTFIXES + parent spec cross-link. EOF )" ``` --- ### Step 4 — Bug #13 active_parsers + active_chunkers (additive minor) #### §2.4.1 Files affected 1. `crates/kebab-store-sqlite/src/store.rs` (또는 lib.rs — `impl SqliteStore` 의 다른 fetch_* method 와 같은 file). 2. `crates/kebab-app/src/schema.rs` (`Models` struct 정의 위치 — `kebab-app` 안 `pub struct Models` 검색해 동일 file 안에 추가). 3. `docs/wire-schema/v1/schema.schema.json` — `models.properties` 에 두 array 추가. 4. `integrations/claude-code/kebab/SKILL.md` — `models` description 갱신. 5. `crates/kebab-app/tests/schema_report.rs` (또는 신규 file) — integration test 2개. #### §2.4.2 Action **(a) `crates/kebab-store-sqlite/src/store.rs` 의 `impl SqliteStore`** — 신규 method 2개: ```rust /// p20-bugfix3 Bug #13: schema.v1.models.active_parsers 의 source. /// `documents.parser_version` 컬럼의 DISTINCT 값을 정렬해 반환. /// 빈 corpus → 빈 Vec. pub fn fetch_distinct_parser_versions(&self) -> anyhow::Result> { let conn = self.conn()?; let mut stmt = conn.prepare( "SELECT DISTINCT parser_version FROM documents WHERE parser_version IS NOT NULL AND parser_version != '' ORDER BY parser_version", )?; let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; let mut out = Vec::new(); for r in rows { out.push(r?); } Ok(out) } /// p20-bugfix3 Bug #13: schema.v1.models.active_chunkers 의 source. /// `chunks.chunker_version` 컬럼의 DISTINCT 값을 정렬해 반환. pub fn fetch_distinct_chunker_versions(&self) -> anyhow::Result> { let conn = self.conn()?; let mut stmt = conn.prepare( "SELECT DISTINCT chunker_version FROM chunks WHERE chunker_version IS NOT NULL AND chunker_version != '' ORDER BY chunker_version", )?; let rows = stmt.query_map([], |row| row.get::<_, String>(0))?; let mut out = Vec::new(); for r in rows { out.push(r?); } Ok(out) } ``` note: `self.conn()` 가 `SqliteStore` 의 기존 connection accessor 가 아니면 같은 file 안 기존 method 의 connection 획득 pattern 을 그대로 사용 (`code_lang_breakdown`, `repo_breakdown`, `corpus_revision` 가 참조 모델). **(b) `crates/kebab-app/src/schema.rs` — `Models` struct 확장**: ```diff pub struct Models { pub parser_version: String, pub chunker_version: String, + /// v0.20.1+ (Bug #13). Corpus 안 활성 parser version 전체. + /// 빈 corpus → empty Vec. backward compat: `parser_version` field 보존. + #[serde(default)] + pub active_parsers: Vec, + /// v0.20.1+ (Bug #13). Corpus 안 활성 chunker version 전체. + /// 빈 corpus → empty Vec. + #[serde(default)] + pub active_chunkers: Vec, pub embedding_version: String, pub prompt_template_version: String, pub index_version: String, pub corpus_revision: u64, } ``` `#[serde(default)]` 는 v0.20.0 이전 client 가 schema.v1 deserialize 시 backward compat (없는 field → `Vec::new()`). **(c) `crates/kebab-app/src/schema.rs:192-207` — `collect_models` 갱신**: ```diff fn collect_models(cfg: &Config, store: &kebab_store_sqlite::SqliteStore) -> Models { + let active_parsers = store.fetch_distinct_parser_versions().unwrap_or_default(); + let active_chunkers = store.fetch_distinct_chunker_versions().unwrap_or_default(); + Models { parser_version: kebab_parse_md::PARSER_VERSION.to_string(), chunker_version: cfg.chunking.chunker_version.clone(), + active_parsers, + active_chunkers, embedding_version: cfg.models.embedding.model.clone(), prompt_template_version: cfg.rag.prompt_template_version.clone(), index_version: kebab_store_vector::INDEX_VERSION_STR.to_string(), corpus_revision: store.corpus_revision(), } } ``` R-3 (spec §6) 해결: `collect_models` 가 매 schema 호출마다 재계산 — cache 없음, stale 위험 없음. **markdown PARSER_VERSION 보존**: 기존 `parser_version` field 는 `kebab_parse_md::PARSER_VERSION` (markdown default) 그대로 — backward compat. spec §3.4 의 결정과 일치. **(d) `docs/wire-schema/v1/schema.schema.json` — `models.properties` 갱신**: ```diff "models": { "type": "object", "required": [ "parser_version", "chunker_version", "embedding_version", "prompt_template_version", "index_version", "corpus_revision" ], "properties": { "parser_version": { "type": "string" }, "chunker_version": { "type": "string" }, + "active_parsers": { + "type": "array", + "items": { "type": "string" }, + "description": "v0.20.1+ (Bug #13). 활성 parser version 전체 (DISTINCT, ORDER BY). 빈 corpus → []. backward-compat: optional, 기존 client 무영향." + }, + "active_chunkers": { + "type": "array", + "items": { "type": "string" }, + "description": "v0.20.1+ (Bug #13). 활성 chunker version 전체 (DISTINCT, ORDER BY). 빈 corpus → []." + }, "embedding_version": { "type": "string" }, "prompt_template_version": { "type": "string" }, "index_version": { "type": "string" }, "corpus_revision": { "type": "integer", "minimum": 0 } } }, ``` `required` array 에는 추가하지 않음 — additive minor 의 정의. **(e) `integrations/claude-code/kebab/SKILL.md:155` — description 갱신**: ```diff -Returns `schema.v1`: `wire.schemas` (supported wire ids), `capabilities` (bool flags — e.g. `streaming_ask`, `rag_multi_turn`), `models` (version cascade 6-axis), `stats` (... +Returns `schema.v1`: `wire.schemas` (supported wire ids), `capabilities` (bool flags — e.g. `streaming_ask`, `rag_multi_turn`), `models` (version cascade 6-axis + v0.20.1 `active_parsers` / `active_chunkers` arrays for multi-version corpora), `stats` (... ``` `description` frontmatter 는 generic 유지 — per-user trigger keyword 는 user 의 local copy 만. #### §2.4.3 New tests `crates/kebab-app/tests/schema_report.rs` (또는 신규 file `crates/kebab-app/tests/schema_active_versions.rs`): ```rust use kebab_app::schema_with_config; use kebab_config::Config; #[test] fn schema_models_active_arrays_empty_on_empty_corpus() { let dir = tempfile::tempdir().unwrap(); let mut cfg = Config::defaults(); cfg.storage.data_dir = dir.path().to_string_lossy().into_owned(); let store = kebab_store_sqlite::SqliteStore::open(&cfg).unwrap(); store.run_migrations().unwrap(); drop(store); let s = schema_with_config(&cfg).unwrap(); assert!(s.models.active_parsers.is_empty(), "empty corpus → no parsers"); assert!(s.models.active_chunkers.is_empty(), "empty corpus → no chunkers"); // backward compat: 기존 단일 field 는 markdown default 보존. assert_eq!(s.models.parser_version, kebab_parse_md::PARSER_VERSION); } #[test] fn schema_emits_active_parsers_and_chunkers_array_after_mixed_ingest() { // markdown + (선택적) code ingest 후 active_parsers/chunkers 가 비어있지 않음. // 본 test 는 kebab-app 의 ingest_with_config + schema_with_config 조합 — 기존 // ingest_lexical.rs / code_ingest_smoke.rs 의 helper fixture 재활용 가능. let dir = tempfile::tempdir().unwrap(); let mut cfg = Config::defaults(); cfg.storage.data_dir = dir.path().to_string_lossy().into_owned(); cfg.workspace.root = { let kb = dir.path().join("kb"); std::fs::create_dir_all(&kb).unwrap(); std::fs::write(kb.join("a.md"), "# A\nhello\n").unwrap(); kb.to_string_lossy().into_owned() }; // Minimal ingest — markdown only 면 active_parsers = ["md-frontmatter-v2"] // (또는 PARSER_VERSION 의 string label) 1 entry. kebab_app::ingest_with_config(&cfg, false).unwrap(); let s = schema_with_config(&cfg).unwrap(); assert!(!s.models.active_parsers.is_empty(), "active_parsers populated after ingest"); assert!(!s.models.active_chunkers.is_empty(), "active_chunkers populated after ingest"); // ORDER BY → sorted (lex order). let mut sorted = s.models.active_parsers.clone(); sorted.sort(); assert_eq!(s.models.active_parsers, sorted, "active_parsers must be sorted"); } ``` note: `kebab_app::ingest_with_config` 정확한 시그니처 (`fn(cfg: &Config, summary_only: bool)` 또는 `fn(scope: SourceScope, summary_only: bool)`) 는 기존 `ingest_lexical.rs` 의 helper 와 동일 pattern 으로 — executor 가 in-tree resolution. #### §2.4.4 Per-step acceptance ```bash cargo test -p kebab-store-sqlite fetch_distinct -j 4 # 신규 store method (있으면) cargo test -p kebab-app schema_models_active_arrays_empty_on_empty_corpus -j 4 cargo test -p kebab-app schema_emits_active_parsers_and_chunkers_array_after_mixed_ingest -j 4 cargo test -p kebab-app schema -j 4 # 기존 schema_report.rs 전수 green (특히 stats_includes_*) cargo clippy -p kebab-store-sqlite -p kebab-app --all-targets -- -D warnings # JSON schema lint (additive minor check) python3 -c "import json; json.load(open('docs/wire-schema/v1/schema.schema.json'))" ``` `stats_includes_breakdowns_and_bytes_on_fresh_corpus` 가 `s.models` 를 assert 안 함 — regression 없음. backward compat: 기존 `parser_version` / `chunker_version` 값 보존. #### §2.4.5 Commit ```bash git add crates/kebab-store-sqlite/src/ \ crates/kebab-app/src/schema.rs \ crates/kebab-app/tests/ \ docs/wire-schema/v1/schema.schema.json \ integrations/claude-code/kebab/SKILL.md git commit -m "$(cat <<'EOF' feat(schema): add active_parsers + active_chunkers arrays to schema.v1.models (Bug #13) 이전: schema.v1.models 가 parser_version / chunker_version 단일 값만 보고 → multi-medium corpus (md + pdf + code Rust/Python + dockerfile + k8s + manifest) 의 version cascade audit 누락 risk. 이후: additive minor — Models struct 에 active_parsers + active_chunkers Vec 추가. backward compat: 기존 단일 field 보존 (markdown default), 신규 array 는 optional (#[serde(default)] + JSON schema required 미포함). source: - kebab_store_sqlite::fetch_distinct_parser_versions() 가 documents.parser_version DISTINCT + ORDER BY 반환. - fetch_distinct_chunker_versions() 가 chunks.chunker_version 동일 pattern. - collect_models 가 매 schema 호출마다 재계산 (cache 없음 — R-3 자동 해결). wire schema additive only — 메이저 bump 불필요. v0.20.1 minor 로 충분. integrations/claude-code/kebab/SKILL.md 동기 갱신. EOF )" ``` --- ### Step 5 — Bug #14 empty query (search + ask) #### §2.5.1 Files affected - `crates/kebab-cli/src/main.rs:818-826` (search arm 의 query_text 해석부). - `crates/kebab-cli/src/main.rs:990` 부근 (ask arm 의 Config::load 직후 — query 변수가 이미 `&String` available). - `crates/kebab-cli/tests/` — 신규 integration test 2개 (또는 `cli_error_wire.rs` 안 추가). #### §2.5.2 Action **(a) search arm** (line 821-826): ```diff // p9-fb-42: bulk mode requires no query; single-query mode requires query. let query_text = match query.as_ref() { - Some(q) => q.clone(), + Some(q) if q.trim().is_empty() => { + return Err(anyhow::Error::new(kebab_app::StructuredError( + kebab_app::ErrorV1 { + schema_version: kebab_app::ERROR_V1_ID.to_string(), + code: "invalid_input".to_string(), + message: "query is empty; provide a non-empty search term or use --bulk".into(), + details: serde_json::Value::Null, + hint: Some("e.g. `kebab search 'rust async'` or `kebab search --bulk < queries.ndjson`".into()), + }, + ))); + } + Some(q) => q.clone(), None => { return Err(anyhow::anyhow!("query is required unless --bulk is set")); } }; ``` `--bulk` mode 우선 — 기존 line 730 의 `if *bulk { ... return Ok(()); }` 가 먼저라 empty query check 가 영향 0. **(b) ask arm** (line 990 의 `let cfg = ...` 직후): ```diff } => { let cfg = kebab_config::Config::load(cli.config.as_deref())?; + if query.trim().is_empty() { + return Err(anyhow::Error::new(kebab_app::StructuredError( + kebab_app::ErrorV1 { + schema_version: kebab_app::ERROR_V1_ID.to_string(), + code: "invalid_input".to_string(), + message: "query is empty; provide a non-empty prompt".into(), + details: serde_json::Value::Null, + hint: Some("e.g. `kebab ask \"explain this code\"`".into()), + }, + ))); + } if *stream { ``` `query: String` (Some 강제 — line 206) 라 `.trim()` 직접 호출 가능. #### §2.5.3 New tests `crates/kebab-cli/tests/cli_empty_query.rs` (신규) 또는 `cli_error_wire.rs` 안: ```rust use std::process::Command; use serde_json::Value; fn kebab_bin() -> String { env!("CARGO_BIN_EXE_kebab").to_string() } fn parse_error_v1(stderr: &str) -> Value { let last = stderr.lines().last().expect("stderr ndjson"); serde_json::from_str(last).unwrap_or_else(|e| panic!("expected ndjson: {e}\n{stderr}")) } #[test] fn search_empty_query_emits_invalid_input() { for q in ["", " "] { let out = Command::new(kebab_bin()) .args(["search", q, "--json"]) .output() .expect("spawn"); assert_ne!(out.status.code(), Some(0), "empty/whitespace query must fail: {q:?}"); let stderr = String::from_utf8_lossy(&out.stderr); let v = parse_error_v1(&stderr); assert_eq!(v["schema_version"], "error.v1"); assert_eq!(v["code"], "invalid_input", "stderr={stderr}"); } } #[test] fn ask_empty_query_emits_invalid_input() { let out = Command::new(kebab_bin()) .args(["ask", "", "--json"]) .output() .expect("spawn"); assert_ne!(out.status.code(), Some(0)); let stderr = String::from_utf8_lossy(&out.stderr); let v = parse_error_v1(&stderr); assert_eq!(v["code"], "invalid_input"); } ``` #### §2.5.4 Per-step acceptance ```bash cargo test -p kebab-cli search_empty_query_emits_invalid_input -j 4 cargo test -p kebab-cli ask_empty_query_emits_invalid_input -j 4 cargo test -p kebab-cli -j 4 # 기존 cli_error_wire / cli_help_smoke / ... 전수 green cargo clippy -p kebab-cli --all-targets -- -D warnings ``` `--bulk < ndjson` 의 empty stdin path 는 spec §2.2 의 별도 case — 본 fix 범위 외 (`bulk` arm 이 query 무시). #### §2.5.5 Commit ```bash git add crates/kebab-cli/src/main.rs crates/kebab-cli/tests/ git commit -m "$(cat <<'EOF' fix(cli): empty query emits error.v1 invalid_input for search + ask (Bug #14) 이전: `kebab search "" --json` / `kebab search " " --json` / `kebab ask "" --json` 모두 exit=0 + silent 0 hit (search) 또는 LLM 빈 prompt round-trip (ask). user mistake (typo, shell expansion 실수) 가 silent → debugging 비용. 이후: 양쪽 arm 에서 `query.trim().is_empty()` → kebab_app::StructuredError (ErrorV1, code=invalid_input, hint 포함). exit=2 (StructuredError → 기존 exit_code() 의 generic non-zero path). --bulk mode 는 영향 0 (bulk arm 이 query 무시). EOF )" ``` --- ### Step 6 — HOTFIXES + parent spec cross-link (Bug #11 deviation) #### §2.6.1 Files affected - `tasks/HOTFIXES.md` — dated entry append. - `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` — inline HTML 주석 1 줄 (PDF OCR config 또는 OQ-1 의 timeout 600 언급 위치 가까이). #### §2.6.2 Action **(a) `tasks/HOTFIXES.md` — append dated subsection**: 기존 HOTFIXES.md "How to add an entry" 섹션 직전에 5-field 형식으로 추가: ```markdown ## 2026-05-27 — PDF OCR `request_timeout_secs` default 600s → 60s (Bug #11) **Discovered**: v0.20.0 final dogfood (2026-05-27), metro-korea.pdf 의 page 8 + 13. **Symptom**: 두 page 모두 `kebab ingest` 가 600s 까지 완전 timeout (`ms: 600000, chars: 0, skipped: true`). 본문 indexed 안 됨, page 당 20분 cost 낭비, user 가 ingest 완료 signal 못 받음. **Root cause**: `default_pdf_ocr_request_timeout_secs() = 600` (parent spec `2026-04-27-kebab-final-form-design.md` §1000 + §1628 OQ-1 의 "CPU 환경 105s 의 5x 여유" 가정). 실측 cloud GPU Ollama 의 per-page throughput 는 6-32s — 600s 까지 가야 timeout 이라면 Ollama 다운 상태가 사실상 확실. 600s 가 fail-fast 신호로 작동 안 함. **Fix** (v0.20.0 bugfix3 round 3, branch `feat/pdf-scanned-ocr`): - `crates/kebab-config/src/lib.rs:477` `default_pdf_ocr_request_timeout_secs() = 60`. - Doc-comment 보강 — 6-32s 정상 throughput, 60s 초과는 Ollama 다운 / 매우 dense·고해상도 page 신호. - User override path 보존 — `config.toml [pdf.ocr] request_timeout_secs = N` 로 늘릴 수 있음 (release notes 에 명문). **Amends**: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §1000 / §1628 OQ-1 (parent spec frozen — text 변경 없음, inline HTML 주석 cross-link 1 줄만 추가). 본 entry 가 live source of truth. ``` 날짜 헤더의 위치는 기존 entries 의 시간순 (가장 최근이 file 위쪽 또는 아래쪽 — 본 file 의 `## 2026-05-01 —` 이후로 이어지는 자리). executor 가 file head + 가장 최근 dated subsection 의 위치 보고 정확한 anchor 삽입. **(b) parent spec inline 주석** — frozen text 보존: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` 의 PDF OCR config block (또는 OQ-1 timeout 언급 위치, brief 의 §1000/§1628 reference) 에 다음 1 줄 HTML 주석 inline 추가: ```markdown ``` 위치: executor 가 `grep -n "request_timeout_secs\|600\|pdf.*timeout\|OQ-1" docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` 로 가장 가까운 anchor 식별 후 그 line 의 직후 또는 같은 paragraph 끝에 inline 으로 삽입. **frozen text 의 prose 자체는 변경 0** — HTML 주석 (``) 은 markdown render 시 invisible. #### §2.6.3 New tests 없음 (docs-only commit). #### §2.6.4 Per-step acceptance ```bash # parent spec 의 prose diff 가 주석 1 줄 외에는 0 인지 확인: git diff docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -E "^[+-][^<]" | head -20 # 위 결과는 모두 "+" 만 보여야 함. # HOTFIXES entry markdown render 검증 (link sanity): python3 -c "import pathlib; t = pathlib.Path('tasks/HOTFIXES.md').read_text(); assert '2026-05-27 — PDF OCR' in t and 'Bug #11' in t" # (optional) markdownlint 가 repo 에 wired 되어 있으면 양쪽 file 에 대해 실행. ``` #### §2.6.5 Commit ```bash git add tasks/HOTFIXES.md docs/superpowers/specs/2026-04-27-kebab-final-form-design.md git commit -m "$(cat <<'EOF' docs(spec): HOTFIXES entry + parent spec cross-link for Bug #11 timeout deviation Bug #11 (이전 commit `fix(config): pdf.ocr.request_timeout_secs default 600 → 60`) 의 frozen-spec deviation handoff. - tasks/HOTFIXES.md: 2026-05-27 dated subsection — Discovered / Symptom / Root cause / Fix / Amends 5-field 포맷 (기존 entries 와 일치). - docs/superpowers/specs/2026-04-27-kebab-final-form-design.md: PDF OCR config block (§1000 / §1628 OQ-1 부근) 에 inline HTML 주석 1 줄 cross-link. prose 변경 0 — parent spec frozen contract 보존, HTML 주석은 markdown render 시 invisible. HOTFIXES entry 가 live source of truth (CLAUDE.md "Spec contract" 규칙). EOF )" ``` --- ### Step 7 — Final sanity (no commit) #### §2.7.1 Workspace-wide check ```bash # 전체 빌드 + clippy 한 번에: cargo build --workspace --release -j 4 cargo clippy --workspace --all-targets -- -D warnings # 전체 test (-j 1 — 18 integration-test binary 의 link OOM 방지): cargo test --workspace --no-fail-fast -j 1 ``` 기준: 기존 1350 test + 본 round 새 +7 test = **1357+ test, 모두 green**. fail 시 step-별로 어디서 regression 인지 isolate. #### §2.7.2 (Optional) Dogfood retest 지금 fresh release binary 가 이미 bugfix2 round 까지 반영 (`/build/out/cargo-target/target/release/kebab`). bugfix3 commit 후 release rebuild + 5 bug 별 수동 smoke: ```bash # Bug #9: capabilities 둘 다 true. kebab schema --json | jq '.capabilities | {streaming_ask, single_file_ingest}' # Bug #10: nonexistent --config → exit≠0 + error.v1 code=config_not_found. kebab search rust --config /tmp/nope.toml --json; echo "exit=$?" # Bug #11: defaults 의 timeout 60. kebab config dump 2>/dev/null | grep request_timeout_secs # (또는 init template 확인) # Bug #13: mixed corpus 에서 active_parsers/chunkers 둘 다 populate. kebab schema --json | jq '.models | {active_parsers, active_chunkers}' # Bug #14: empty query 양쪽 모두 invalid_input. kebab search "" --json; echo "exit=$?" kebab ask "" --json; echo "exit=$?" ``` dogfood 는 optional — workspace test green + clippy clean 가 commit 의 충분 조건. dogfood 결과는 final round 의 review 단계에서 캡쳐. #### §2.7.3 Branch state ```bash git log --oneline -7 # (예상) #
docs(spec): HOTFIXES entry + parent spec cross-link for Bug #11 timeout deviation #
fix(cli): empty query emits error.v1 invalid_input for search + ask (Bug #14) #

feat(schema): add active_parsers + active_chunkers arrays to schema.v1.models (Bug #13) #

fix(config): pdf.ocr.request_timeout_secs default 600 → 60 per dogfood evidence (Bug #11) #

fix(config): emit error.v1 code=config_not_found for missing --config path (Bug #10) #

fix(app): flip streaming_ask + single_file_ingest capabilities to actual surface (Bug #9) # f763049 test(cli): assert 'code' in search --help output (Bug #7 regression pin) ``` 6 commit (Step 6 = 1 doc commit). PR 는 `feat/pdf-scanned-ocr` branch 그대로 force-update (base=main, spec §0 의 "PR #189 force-update" 참조). --- ## §3 Verifier checklist cumulative — Step 7 까지 진행 후 verifier 가 점검. | # | Criterion | Command | Expected | Spec AC | |----|----------|---------|----------|---------| | V-1 | capabilities flag 둘 다 true (`streaming_ask` + `single_file_ingest`) | `cargo test -p kebab-app capabilities_streaming_ask_matches_cli_surface capabilities_single_file_ingest_matches_cli_surface -j 4` | green | AC-1 | | V-2 | absolute missing `--config` → exit≠0 + error.v1 code=config_not_found | `cargo test -p kebab-cli invalid_config_path_emits_error_v1_with_nonzero_exit -j 4` | green | AC-2 | | V-3 | relative missing `--config` → exit≠0 + error.v1 code=config_not_found | `cargo test -p kebab-cli invalid_relative_config_path_emits_config_not_found -j 4` | green | AC-7 | | V-4 | OCR timeout default 60s | `cargo test -p kebab-config pdf_ocr_request_timeout_default_is_60s -j 4` | green | AC-3 | | V-5 | active_parsers/chunkers populate on mixed corpus + 빈 corpus 빈 array | `cargo test -p kebab-app schema_emits_active_parsers_and_chunkers_array_after_mixed_ingest schema_models_active_arrays_empty_on_empty_corpus -j 4` | green | AC-4 | | V-6 | empty query (search "" + " ") → invalid_input | `cargo test -p kebab-cli search_empty_query_emits_invalid_input -j 4` | green | AC-5 | | V-7 | empty query (ask "") → invalid_input | `cargo test -p kebab-cli ask_empty_query_emits_invalid_input -j 4` | green | AC-6 | | V-8 | workspace test 전수 green | `cargo test --workspace --no-fail-fast -j 1` | exit=0, 1357+ test green | AC-8 | | V-9 | clippy clean | `cargo clippy --workspace --all-targets -- -D warnings` | exit=0, warn 0 | AC-7 spec layer | | V-10 | wire schema additive minor valid (parse + required 유지) | `python3 -c "import json; s=json.load(open('docs/wire-schema/v1/schema.schema.json')); assert 'active_parsers' in s['properties']['models']['properties']; assert 'active_parsers' not in s['properties']['models']['required']"` | exit=0 | AC-9 | | V-11 | SKILL.md 동기 갱신 (active_* 언급) | `grep -q "active_parsers\|active_chunkers" integrations/claude-code/kebab/SKILL.md` | exit=0 | AC-10 | | V-12 | HOTFIXES entry + parent spec cross-link 존재 | `grep -q "2026-05-27 — PDF OCR" tasks/HOTFIXES.md && grep -q "HOTFIX 2026-05-27: pdf.ocr" docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` | exit=0 | AC-8 spec layer | | V-13 | parent spec prose 변경 0 (HTML 주석만) | `git diff main -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md \| grep -E "^[+-][^<]" \| wc -l` | 0 | constraint §7.2 | | V-14 | (optional manual) `kebab ask --stream` regression 없음 | `kebab ask --stream "what is rust" 2>&1 \| head -3` | answer_event.v1 ndjson | AC-10 manual | V-1 ~ V-12 자동, V-13 자동 (diff line count 0), V-14 manual / optional. --- ## §4 Risks resolution (spec §6 의 plan-level) | Risk | Resolution | Step | Verification | |------|-----------|------|--------------| | R-1 — config layer 순서 (kebab-config error vs kebab-app classify) | spec §3.2 의 (a) 선택: `kebab-config` 자체 error type + `kebab-app` downcast. 기존 `ConfigInvalid` pattern 그대로 mirror (re-export 4 file: error_signal.rs, lib.rs, error_wire.rs, classify). | Step 2 | V-2 + V-3 + 기존 error_wire test (config_invalid_classifies_to_config_invalid_code) 보존. | | R-2 — config relative path (cwd-relative) | `std::path::Path::exists()` 는 cwd-relative — 별도 작업 0. integration test 가 `tempfile::tempdir() + current_dir(...)` 로 absolute / relative 양쪽 cover. | Step 2 | V-3. | | R-3 — active_* cache invalidation | `collect_models` 가 매 schema 호출마다 store query 직접 — cache 없음. R-3 N/A. | Step 4 | V-5 (빈 corpus + mixed 둘 다). 향후 caching 추가 시 corpus_revision invalidation 명문. | | R-4 — corpus shrink 시 stale | 위와 동일 (every-call 재계산). | Step 4 | V-5. | | R-5 — 60s 도 dense/고해상도 page timeout 가능 | mitigation: config.toml `[pdf.ocr] request_timeout_secs = N` override path 유지. 새 release notes 명문 (v0.20.1 minor bump 시). | Step 3 + Step 6 | V-4 (default value test). release notes 는 본 plan 범위 외 — gitea-release 시 cover. | 추가 OQ — Step 2 의 ConfigNotFound 가 `Config::from_file` 의 read_failed (file 존재하나 IO error) 와 구분되는가? **결정**: 본 fix 는 `!p.exists()` path 만 처리. file 은 존재하나 permission denied 등 IO error 는 기존 `ConfigInvalid::read_failed` (line 729-733) path 그대로 — 두 error 가 명확히 disjoint. --- ## §5 Open questions for executor 1. **Step 2 (b)** — `ConfigNotFound` 추가 시 `crates/kebab-app/src/error_signal.rs` 의 정확한 export shape: 기존 `pub use kebab_config::ConfigInvalid;` 가 그 file 에 있는지 vs `lib.rs` 직접 인지 grep 으로 확인. 기존 pattern 그대로 mirror. 2. **Step 3** — 기존 unit test 중 `request_timeout_secs.*600` 가 있는지 `grep -rn "request_timeout_secs.*600\|600.*request_timeout_secs" crates/kebab-config/` 로 사전 확인. 발견 시 같은 commit 안에서 expect 값을 60 으로 갱신 (별도 commit 금지 — atomic). 3. **Step 4 (a)** — `SqliteStore::conn()` 의 정확한 method 이름이 다른 file 에 따라 (`get_conn`, `connection`, `with_conn` 등) 다를 수 있음. 기존 `code_lang_breakdown` / `repo_breakdown` impl 옆에 같은 pattern 으로 추가 — connection 획득 line 그대로 복붙. 4. **Step 4 (e)** — `integrations/claude-code/kebab/SKILL.md` 의 schema description 갱신 범위: brief §0.1 의 "6-axis → 8-axis 또는 '+ active arrays'" 중 후자 채택 (6-axis 라는 표현이 다른 곳에 인용될 수 있어 number 를 늘리는 대신 "+ array" 추가 형식 — backward compat 표현). 5. **Step 6 (b)** — parent spec inline 주석의 정확한 anchor: brief 의 "§1000 / §1628 OQ-1" 은 spec body 안 section number 가 아닌 line number 일 가능성. executor 가 `wc -l docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` + `grep -n "request_timeout_secs\|OQ-1\|600" docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` 결과로 가장 가까운 anchor 식별 후 inline insertion. **prose 변경 0** 이 핵심 invariant. 6. **Step 5 — `--bulk` precedence**: 기존 line 730 (`Some(bulk)` 분기) 이 query empty check 보다 먼저인지 확인. 본 plan 은 그렇다고 가정 (line 818 에서 cfg load 가 일어나기 전에 bulk return) — false 면 bulk path 가 empty check 의 영향을 받음. --- ## §6 References - spec: `docs/superpowers/specs/2026-05-27-v0.20-sub1-bugfix3-spec.md` (410 line, ACCEPT 11/11) - brief: `.omc/reviews/2026-05-27-v0.20-bugfix3-plan-drafter-brief.md` - prior critic rounds: - `.omc/reviews/2026-05-27-v0.20-bugfix3-spec-closure-result.md` - `.omc/reviews/2026-05-27-v0.20-bugfix3-spec-closure-r2-result.md` - source dogfood report: `.omc/reviews/2026-05-27-v0.20-final-dogfood-report.md` - code anchors: - `crates/kebab-app/src/schema.rs:137-151` (capabilities_snapshot) - `crates/kebab-app/src/schema.rs:192-207` (collect_models) - `crates/kebab-config/src/lib.rs:19-22` (ConfigInvalid pattern model) - `crates/kebab-config/src/lib.rs:477` (default_pdf_ocr_request_timeout_secs) - `crates/kebab-config/src/lib.rs:688-722` (Config::load) - `crates/kebab-app/src/error_wire.rs:49-104` (classify) - `crates/kebab-cli/src/main.rs:206` (Ask query: String) - `crates/kebab-cli/src/main.rs:718` (Cmd::Search) - `crates/kebab-cli/src/main.rs:818-826` (search arm Config::load + query_text) - `crates/kebab-cli/src/main.rs:977-990` (Cmd::Ask) - `docs/wire-schema/v1/schema.schema.json:30-44` (Models object) - `integrations/claude-code/kebab/SKILL.md:155` (schema description) - `tasks/HOTFIXES.md` (dated entries pattern) --- ## §7 Constraints (spec §7 mirror) 1. **branch 변경 0** — 모든 commit 이 `feat/pdf-scanned-ocr` 에 올라감. base=main 의 force-update 만 (Step 7 후 PR push). 2. **spec ACCEPT (frozen contract) 변경 0** — `docs/superpowers/specs/2026-05-27-v0.20-sub1-bugfix3-spec.md` 본 plan 안에서 read-only. 3. **regression 0** — 기존 workspace test (~1350) 전수 green + 본 round 새 +7 test 추가 (총 1357+). 4. **wire schema = additive minor** — Bug #13 의 `active_parsers` / `active_chunkers` 는 optional, JSON schema `required` 미포함, `#[serde(default)]`. v0.20.1 minor bump 로 충분. 5. **parent spec text 변경 = inline HTML 주석 1 줄만** — frozen prose 보존, HOTFIXES.md 가 live source of truth. 6. **subagent skip** — direct in-session 작성, nested worker spawn 금지 (worker protocol). 7. **commit message style**: 기존 commit log (`f763049 test(cli): ...`, `8cf73d1 docs(cli): ...`, `a58ee10 fix(parse-pdf): ...`) 의 `kind(scope): subject (Bug #N)` pattern 그대로. body 는 Why + What — 본 plan 의 commit block 그대로 사용. 8. **estimated time**: 60 min — Step 1 (5min) + Step 2 (15min) + Step 3 (5min) + Step 4 (20min) + Step 5 (10min) + Step 6 (5min) + Step 7 sanity. spec 의 30-45 min 보다 보수적. --- _End of plan._