Files
kebab/docs/superpowers/plans/2026-05-27-v0.20-sub1-bugfix3-plan.md
altair823 46e99470eb docs(superpowers): v0.20 sub-item 1 bugfix1/2/3 specs + plans + DOGFOOD.md
3-round dogfood-driven fix cycle 의 산출물:

- bugfix1 (Bug #2/#3/#4): spec 964 line + plan 848 line
- bugfix2 (Bug #6/#7, #8 falsified): spec 308 line + plan 388 line
- bugfix3 (Bug #9/#10/#11/#13/#14, #12 falsified): spec 410 line + plan 1043 line
- docs/DOGFOOD.md: 전방위 dogfood checklist 의 전체 (§0 environment ~ §13 reference corpus)

각 round 의 spec/plan 가 critic + verifier round 2 closure ACCEPT 후 frozen. dogfood-driven evidence 기반.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 01:21:34 +00:00

48 KiB
Raw Blame History

title, created, status, round, spec_path, parent_spec, brief, branch, base_head, step_count, commit_count, estimated_minutes
title created status round spec_path parent_spec brief branch base_head step_count commit_count estimated_minutes
v0.20.0 sub-item 1 bugfix round 3 — plan 2026-05-27 DRAFT 0 docs/superpowers/specs/2026-05-27-v0.20-sub1-bugfix3-spec.md docs/superpowers/specs/2026-04-27-kebab-final-form-design.md .omc/reviews/2026-05-27-v0.20-bugfix3-plan-drafter-brief.md feat/pdf-scanned-ocr f763049 7 6 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

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 의 두 줄만 변경:

 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 개 추가:

#[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 <path> + kebab ingest-stdin --title <T> 양쪽 모두
    // 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_snapshotpub(crate) 또는 module-internal 일 경우 super:: path 로 접근. private 라면 같은 module 의 child mod 에서 호출 가능.

§2.1.4 Per-step acceptance

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_extstats_includes_breakdowns_and_bytes_on_fresh_corpus (schema_with_config 경유) 가 streaming_ask/single_file_ingest 를 assert 안 함 — regression 없음.

§2.1.5 Commit

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 <path> / kebab ingest-stdin --title <T> → 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::loadSome(_) => 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
    • classifyConfigInvalid 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 직후):

/// p20-bugfix3 Bug #10: explicit `--config <path>` 가 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-722Config::load 분기 수정:

 pub fn load(path: Option<&Path>) -> anyhow::Result<Self> {
     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:

pub use kebab_config::ConfigNotFound;

(기존 ConfigInvalid re-export 와 동등 위치. 같은 file 안에서 use kebab_config::ConfigInvalid; 이미 있다면 그 옆.)

(d) crates/kebab-app/src/error_wire.rs::classifyConfigInvalid arm 직후:

if let Some(s) = err.downcast_ref::<ConfigNotFound>() {
    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 <path>; 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:

pub use kebab_config::{ConfigInvalid, ConfigNotFound};

§2.2.3 New tests

(a) crates/kebab-config/src/lib.rs — unit test (기존 tests mod 안):

#[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::<ConfigNotFound>()
        .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 개:

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

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::loadNone → XDG default path 는 변경 0 — kebab doctor (config 없는 fresh clone) regression 없음.

§2.2.5 Commit

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

-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:

#[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

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

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-apppub struct Models 검색해 동일 file 안에 추가).
  3. docs/wire-schema/v1/schema.schema.jsonmodels.properties 에 두 array 추가.
  4. integrations/claude-code/kebab/SKILL.mdmodels description 갱신.
  5. crates/kebab-app/tests/schema_report.rs (또는 신규 file) — integration test 2개.

§2.4.2 Action

(a) crates/kebab-store-sqlite/src/store.rsimpl SqliteStore — 신규 method 2개:

/// 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<Vec<String>> {
    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<Vec<String>> {
    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.rsModels struct 확장:

 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<String>,
+    /// v0.20.1+ (Bug #13). Corpus 안 활성 chunker version 전체.
+    /// 빈 corpus → empty Vec.
+    #[serde(default)]
+    pub active_chunkers: Vec<String>,
     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-207collect_models 갱신:

 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.jsonmodels.properties 갱신:

     "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 갱신:

-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):

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

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_corpuss.models 를 assert 안 함 — regression 없음. backward compat: 기존 parser_version / chunker_version 값 보존.

§2.4.5 Commit

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<String>
추가. 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):

             // 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 = ... 직후):

         } => {
             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 안:

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

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

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
)"

§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 형식으로 추가:

## 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 추가:

<!-- HOTFIX 2026-05-27: pdf.ocr.request_timeout_secs default 60s (Bug #11). See tasks/HOTFIXES.md 2026-05-27 entry. -->

위치: 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

# parent spec 의 prose diff 가 주석 1 줄 외에는 0 인지 확인:
git diff docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -E "^[+-][^<]" | head -20
# 위 결과는 모두 "+<!-- HOTFIX 2026-05-27 ... -->" 만 보여야 함.

# 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

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

# 전체 빌드 + 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:

# 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

git log --oneline -7
# (예상)
# <h6> docs(spec): HOTFIXES entry + parent spec cross-link for Bug #11 timeout deviation
# <h5> fix(cli): empty query emits error.v1 invalid_input for search + ask (Bug #14)
# <h4> feat(schema): add active_parsers + active_chunkers arrays to schema.v1.models (Bug #13)
# <h3> fix(config): pdf.ocr.request_timeout_secs default 600 → 60 per dogfood evidence (Bug #11)
# <h2> fix(config): emit error.v1 code=config_not_found for missing --config path (Bug #10)
# <h1> 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) 변경 0docs/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.