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>
48 KiB
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_snapshot 가 pub(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_ext 의 stats_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
crates/kebab-config/src/lib.rs- 19-22 line 의
ConfigInvalid정의 옆에ConfigNotFound추가. - 688-722 line 의
Config::load안Some(_) => Self::defaults(),arm 을Some(_) => Err(...)로 변경.
- 19-22 line 의
crates/kebab-app/src/error_signal.rspub use kebab_config::ConfigNotFound;추가 (기존ConfigInvalid와 동등 pattern).
crates/kebab-app/src/error_wire.rsclassify안ConfigInvalidarm 다음에ConfigNotFoundarm 추가.
crates/kebab-app/src/lib.rspub 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-722 — Config::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::classify — ConfigInvalid 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::load 의 None → 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
crates/kebab-store-sqlite/src/store.rs(또는 lib.rs —impl SqliteStore의 다른 fetch_* method 와 같은 file).crates/kebab-app/src/schema.rs(Modelsstruct 정의 위치 —kebab-app안pub struct Models검색해 동일 file 안에 추가).docs/wire-schema/v1/schema.schema.json—models.properties에 두 array 추가.integrations/claude-code/kebab/SKILL.md—modelsdescription 갱신.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개:
/// 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.rs — Models 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-207 — collect_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.json — models.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_corpus 가 s.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 변수가 이미&Stringavailable).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
)"
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 형식으로 추가:
## 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
- Step 2 (b) —
ConfigNotFound추가 시crates/kebab-app/src/error_signal.rs의 정확한 export shape: 기존pub use kebab_config::ConfigInvalid;가 그 file 에 있는지 vslib.rs직접 인지 grep 으로 확인. 기존 pattern 그대로 mirror. - 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). - Step 4 (a) —
SqliteStore::conn()의 정확한 method 이름이 다른 file 에 따라 (get_conn,connection,with_conn등) 다를 수 있음. 기존code_lang_breakdown/repo_breakdownimpl 옆에 같은 pattern 으로 추가 — connection 획득 line 그대로 복붙. - 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 표현). - 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. - Step 5 —
--bulkprecedence: 기존 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)
- branch 변경 0 — 모든 commit 이
feat/pdf-scanned-ocr에 올라감. base=main 의 force-update 만 (Step 7 후 PR push). - spec ACCEPT (frozen contract) 변경 0 —
docs/superpowers/specs/2026-05-27-v0.20-sub1-bugfix3-spec.md본 plan 안에서 read-only. - regression 0 — 기존 workspace test (~1350) 전수 green + 본 round 새 +7 test 추가 (총 1357+).
- wire schema = additive minor — Bug #13 의
active_parsers/active_chunkers는 optional, JSON schemarequired미포함,#[serde(default)]. v0.20.1 minor bump 로 충분. - parent spec text 변경 = inline HTML 주석 1 줄만 — frozen prose 보존, HOTFIXES.md 가 live source of truth.
- subagent skip — direct in-session 작성, nested worker spawn 금지 (worker protocol).
- 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 그대로 사용. - 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.