design §3.7b 의 thin layer (ParsedBlock 류) 가 4 parser 중 1개 (markdown) 만 lift 를 경유하는 현실 — fan-in/fan-out 모두 1 → layer 의미 잃음. kebab-normalize (1097 LOC) + kebab-parse-types (98 LOC) 둘을 kebab-parse-md 로 흡수. 설계: docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md 플랜: docs/superpowers/plans/2026-05-26-normalize-absorption-plan.md HOTFIXES: tasks/HOTFIXES.md 의 2026-05-26 entry (design deviation) - 5 사용 type + 3 forward-declared struct → kebab-parse-md::types module 의 pub explicit re-export. - build_canonical_document + derive_title + warning_agent → kebab-parse-md::normalize module. - 4 hard-coded agent literal (lib.rs:122/128/134/153) + warning_agent body return + tracing target literal 모두 보존 — stage label 일관성. - kebab-app callsite (lib.rs:51 use + :1119 context string) + Cargo.toml 의 2 dep (regular + dead) 제거. - kebab-chunk + kebab-store-sqlite 의 [dev-dependencies] kebab-normalize → 제거 (kebab-parse-md 로 갈음). 통합 test source 의 use shift. - test file 이동 (kebab-normalize/tests/normalize_snapshot.rs → kebab-parse-md/tests/). - workspace Cargo.toml: Hunk (a) members 2 entry 삭제 + Hunk (b) version 0.18.0 → 0.19.0 (frozen contract 변경). - design §3.7b 4-단락 재작성 (원래 intent 보존 + 현재 상태 + 보존된 surface + future re-extraction trigger). - design §8 graph 갱신 (3 edge 제거 + 2 forbidden bullet 의미 갱신 + commentary). - ARCHITECTURE.md crate graph + directory tree mechanical 갱신. - tasks/INDEX.md L169 closure mention + "Future work / deferred" 섹션 신설 (image/pdf normalize integration entry). - tasks/HOTFIXES.md 신규 entry (4-block — design deviation Symptom). - HANDOFF.md cross-link 한 줄. - 3 dead struct (ParsedImageRegion / ParsedPdfPage / ParsedAudioSegment) 는 보존 — v0.20+ image/pdf normalize integration 의 future surface (spec §11). Wire / surface impact: 0건. CLI / TUI / MCP / --json 출력 / config / XDG path / parser_version 모두 unchanged. wire-invisible provenance.events[].agent + tracing target literal "kb-normalize" 도 보존 — old DB row 와 new DB row 의 audit log 일관성. Verification: cargo test --workspace --no-fail-fast -j 1 → 1313 passed / 0 failed (172 result blocks). cargo clippy --workspace --all-targets -j 1 -- -D warnings → 0 warning (5m 46s). cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length' = 22. cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)" = 0 줄.
78 KiB
status, target_version, spec, contract_sections, related_specs, related_plans, hotfix_links
| status | target_version | spec | contract_sections | related_specs | related_plans | hotfix_links | ||||
|---|---|---|---|---|---|---|---|---|---|---|
| drafting | 0.19.0 | docs/superpowers/specs/2026-04-27-kebab-final-form-design.md (§3.7b 재작성 + §8 graph 갱신) |
|
|
kebab-normalize + kebab-parse-types 흡수 — 24 → 22 crates + §3.7b 재작성
§1 Background + evidence chain
§1.1 현재 24 crate workspace
Cargo.toml 의 [workspace] members 가 24 crate 를 declare (확인: head -30 Cargo.toml). 이 중 둘이 본 refactor 의 대상이다.
crates/kebab-normalize— 1097 LOC (lib.rs only,wc -l측정), production caller 1개 (kebab-app).crates/kebab-parse-types— 98 LOC (lib.rs only), production caller 2개 (kebab-parse-md,kebab-normalize), kebab-app 에서 dep declare 했지만 import 0건 (dead dep).
PR #181 (post-PR9 refactor) 머지 직전 system-architect 의 component-level review 가 "pre-cut nothing, all v0.18.1+ defer (kebab-normalize 흡수, Extractor dispatch unification, kebab-source-fs dep lightening 등)" 결론 (tasks/INDEX.md L169). Sub-item 1 (kebab-source-fs dep lightening) 은 이미 PR #185 로 머지됨. 본 spec 은 sub-item 2 ("kebab-normalize 흡수").
§1.2 kebab-normalize caller 실측
production source 와 dev-deps 둘 다 명시 (actual Cargo.toml 의 [dev-dependencies] block 인용 — cat crates/{kebab-chunk,kebab-store-sqlite,kebab-normalize}/Cargo.toml | grep -A20 "dev-dependencies"):
| crate | callsite | 종류 |
|---|---|---|
kebab-app |
src/lib.rs:51 (use kebab_normalize::build_canonical_document;) |
production |
kebab-chunk |
Cargo.toml [dev-dependencies] kebab-normalize (snapshot integration test 의 fixture builder) |
dev-only |
kebab-store-sqlite |
Cargo.toml [dev-dependencies] kebab-normalize (contract round-trip test 의 fixture builder) |
dev-only |
kebab-normalize (자체) |
tests/normalize_snapshot.rs 가 Cargo.toml [dev-dependencies] kebab-parse-md 로 reverse-direction dev-dep — kebab-parse-md 를 fixture parser 로 사용 |
reverse dev-dep |
→ production caller = kebab-app 단일. kebab-normalize 흡수 시:
kebab-app의 1 줄 use statement + call site (lib.rs:51, :1119) 갱신.kebab-chunk+kebab-store-sqlite의 dev-depkebab-normalize→kebab-parse-md로 갈음 + 두 crate 의 통합 test source (tests/*.rs) 의use kebab_normalize::*;→use kebab_parse_md::*;갱신.kebab-normalize/tests/normalize_snapshot.rs의kebab-parse-mdreverse dev-dep 은 흡수 후 자기 자신 참조 → declare 제거 (in-crate test 가 lib 를 자동 link,Q4의 cargo standard behavior — §6.3 의 R3 verified).
이 4 surface 가 본 PR 의 callsite migration scope 전체.
§1.3 kebab-parse-types caller 실측
grep -rn "kebab_parse_types" --include="*.rs" crates/*/src/ (production source 만):
| crate | use 횟수 | 사용 type | 종류 |
|---|---|---|---|
kebab-parse-md |
다수 (blocks.rs, frontmatter.rs) |
ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind |
production |
kebab-normalize |
다수 (lib.rs) |
위 5 type 동일 | production |
kebab-app |
Cargo.toml declare, .rs use 0건 |
(없음) | dead dep |
→ production caller 2개 (kebab-parse-md, kebab-normalize) + kebab-app 의 dead dep 1건. kebab-normalize 가 흡수되면 caller 가 kebab-parse-md 단일로 collapse — kebab-parse-types 의 raison d'être (parser 와 normalize 사이의 layer) 소멸.
§1.4 4 parser 의 normalize / parse-types 의존 실측
| parser crate | kebab-normalize 의존? |
kebab-parse-types 의존? |
extract 결과 |
|---|---|---|---|
kebab-parse-md |
(자체 의존 없음 — kebab-normalize 는 역방향 으로 parse-md 를 dev-dep 으로 사용, §1.2 참조) |
production (Cargo.toml:12) |
Vec<ParsedBlock> (normalize 경유 → CanonicalDocument) |
kebab-parse-pdf |
0 (production 0, dev-deps 0) | 0 | CanonicalDocument 직접 emit |
kebab-parse-image |
0 | 0 | CanonicalDocument 직접 emit |
kebab-parse-code |
0 | 0 | CanonicalDocument 직접 emit |
→ design §3.7b 의 의도 ("ParsedBlock 류는 모든 parser 가 emit → normalize 가 일괄 lift") 와 현실 (markdown 만 normalize 통과, 나머지 3 parser 는 CanonicalDocument 직접 emit) 가 divergent. kebab-normalize 의 production caller 가 1개 (kebab-app 단일) 인 이유도 동일 — 4 parser 중 1개만 normalize 를 경유.
중요: kebab-parse-audio crate 는 미존재 (P8 audio 가 사용자 결정으로 deferred — tasks/INDEX.md 의 Phase 8 row 참조). 본 spec 의 mission wording 도 5 parser 가 아닌 4 parser 기준.
§1.5 kebab-normalize surface (1097 LOC, lib.rs only)
actual source 의 정확한 signature (crates/kebab-normalize/src/lib.rs:60-66, :360 — tasks/p1/p1-4-normalize.md:54-62 frozen 과 byte-identical):
pub fn build_canonical_document(
asset: &RawAsset, // by-ref, kebab_core::RawAsset (NOT &AssetInfo)
metadata: Metadata, // by-value, kebab_core::Metadata (NOT &Metadata)
blocks: Vec<ParsedBlock>,
parser_version: &ParserVersion,
warnings: Vec<Warning>,
) -> Result<CanonicalDocument>;
pub fn derive_title(
frontmatter_title: &str, // &str by-ref (NOT Option<&str>)
blocks: &[Block], // lifted Block, NOT ParsedBlock — lift 이후 호출
file_stem: &str, // &str by-ref (NOT Option<&str>)
) -> String;
pub use kebab_core::{id_for_block, id_for_doc};
build_canonical_document— markdown 의Vec<ParsedBlock>을 받아CanonicalDocument로 lift. ID 생성 / Provenance event 누적 / Warning → ProvenanceEvent 변환 /warning_agent분기 (md vs lift-stage attribution).metadata는 by-value — 내부에서usermap 의title/lang을remove하면서 lift (mutating ownership, wire 중복 회피).derive_title— p9-fb-07 frozen API (markdown title fallback chain).tasks/p9/p9-fb-07-md-title-fallback.md가 본 함수 의 정확한 contract 를 freeze. 중요:blocks: &[Block]는 lift 된kebab_core::Block이며 (lift 이전 의ParsedBlock아님), 따라서 본 함수는build_canonical_document의 내부 에서 lift 후 호출됨 — caller 입장 에서derive_title의 direct call 시점 에는 이미Vec<Block>가 손에 있어야 함. 본 의미가 plan / executor 의 type-error misassumption 방지의 핵심.- 1097 LOC 중 ~700 LOC = unit tests. production fn body 는 ~400 LOC.
p1-4 frozen cross-link: tasks/p1/p1-4-normalize.md:54-62 의 signature block 이 본 §1.5 의 정확한 source-of-truth. 본 spec 의 §3.7 callsite migration 은 signature 자체를 변경하지 않으므로 frozen 위반 0.
§1.6 kebab-parse-types surface (98 LOC, lib.rs only)
production 에서 사용 중 (5 type):
ParsedBlock,ParsedBlockKind,ParsedPayload— markdown parser → normalize 의 lift input.Warning,WarningKind— markdown parser 의 panic-recovery + table malformed + frontmatter malformed event.
forward-declared, production caller 0 (3 struct):
ParsedImageRegion(line 85) — P6 image stage 의 intent 표현. 현 surface =pub struct ParsedImageRegion;(unit struct, payload 없음).ParsedPdfPage(line 88) — P7 pdf stage 의 intent. surface =pub struct ParsedPdfPage { pub page: u32, pub text: String }.ParsedAudioSegment(line 94) — P8 audio (deferred) 의 intent. surface =pub struct ParsedAudioSegment { pub start_ms: u64, pub end_ms: u64, pub text: String }.
→ 3 dead struct 의 design intent 자체 ("multi-region image, multi-block pdf, multi-segment audio 의 lift surface") 는 유효. 사용자 결정 (team-lead 메시지 의 #6): 보존 + future surface 명시.
§1.7 design §3.7b 의 abstraction reality check
design §3.7b (docs/superpowers/specs/2026-04-27-kebab-final-form-design.md:703-764) 가 kebab-parse-types 의 raison d'être 를 다음 2 점으로 정당화:
- namespace 폭발 방지: parser-별 ParsedBlock 변종이
kebab-core의 namespace 를 폭발시키지 않도록 thin layer 분리. - normalize 의 parser non-dependence: normalize 가 어떤 parser 도 직접 import 하지 않아야 함.
현실 (P1~P10 머지 후):
- 4 parser 중 3개 (pdf / image / code) 가
CanonicalDocument직접 emit → normalize 경유 안 함 → "normalize 가 parser 직접 import 하면 안 됨" 의 constraint 가 markdown 한 갈래에서만 의미 존재. kebab-core::Blocknamespace 폭발 우려도 4 parser 중 3개가 우회 →kebab-parse-types의 layer 가 현 시점 에 막아야 할 폭발이 markdown 외엔 없음.- §3.7b 의 "thin layer 가 유일한 합류 지점" — 합류 지점에 들어오는 parser 가 1개 (md) → layer 무용. fan-in/fan-out 둘 다 1.
그러나 design intent 자체 ("향후 image/pdf/audio 가 normalize 거치도록 바뀔 경우 layer 가 다시 살아남") 는 valid future contingency. 따라서 §3.7b 를 완전 strike 가 아닌 재작성 하는 것이 정확 — "thin layer 는 현재 markdown 외 parser 가 사용하지 않으므로 kebab-parse-md 흡수. 3 forward-declared struct 는 보존되며 future re-extraction trigger 명시".
§1.8 사용자 결정 요약 (team-lead 메시지의 7 점)
| # | 결정 | 본 spec 반영 위치 |
|---|---|---|
| 1 | target_version = 0.19.0 (frozen contract 변경) | frontmatter + §7 |
| 2 | Destination = Option A (kebab-parse-md 흡수) |
§3.1 |
| 3 | ~25 referencing task spec = frozen 유지 + HOTFIXES live source | §7 (mechanical update 0) |
| 4 | kebab-app 의 dead kebab-parse-types dep = 같은 PR incidental cleanup |
§3.10 |
| 5 | HOTFIXES entry = 4-block 변형 (Symptom → "design deviation") | §3.9 |
| 6 | 3 dead struct 보존 + future surface 명시 | §3.3 (보존) + §6 (future trigger) |
| 7 | warning_agent wire visibility = planner 가 검증 → 결과 반영 |
§1.9 + §7 |
§1.9 wire visibility 검증 결과 (sub-item 2 의 detective verification)
wire 의 정의 (본 spec 범위 내): "wire" = JSON-RPC payload (kebab-mcp 의 tool definitions) + CLI --json output (kebab-app facade) + 외부 통합 의 schema (Claude Code skill 등). SQLite BLOB persistence (documents.provenance_json) 는 wire 외 — 단일 사용자 의 local DB 의 internal storage, 외부 통합 없음.
검증 방법: docs/wire-schema/v1/*.json 의 16 file 모두 (ls docs/wire-schema/v1/*.json | wc -l = 16) 의 agent / provenance field 명시적 검색 + production code 의 agent field flow trace.
| wire schema | agent field? |
provenance field? |
|---|---|---|
answer_event.v1 |
0 | 0 |
answer.v1 |
0 | 0 |
bulk_search_item.v1 |
0 | 0 |
bulk_search_response.v1 |
0 | 0 |
chunk_inspection.v1 |
0 | 0 |
citation.v1 |
0 | 0 |
doc_summary.v1 |
0 | 0 |
doctor.v1 |
0 | 0 |
error.v1 |
0 | 0 |
fetch_result.v1 |
0 | 0 |
ingest_progress.v1 |
0 | 0 |
ingest_report.v1 |
0 | 0 |
reset_report.v1 |
0 | 0 |
schema.v1 |
0 | 0 |
search_hit.v1 |
0 | 0 |
search_response.v1 |
0 | 0 (description 산문 의 "MCP / agent consumers" mention 은 line 35 의 hint field description 내 — field 아님, false-positive 명시) |
False-positive 명시: docs/wire-schema/v1/search_response.schema.json:35 의 hint field description 본문에 "MCP / agent consumers should surface this" 문구 1 hit (description prose, schema field 아님). 본 hit 는 ProvenanceEvent.agent 와 무관 — hint 는 short-query advisory string (v0.17.0 A5).
production flow trace (actual crates/kebab-normalize/src/lib.rs 의 emission point 별 분리):
| line | string | emitter | persist |
|---|---|---|---|
:109 |
"kebab-normalize" |
tracing::debug! target literal |
(log file, NOT SQLite — ~/.local/state/kebab/logs/kb.log.YYYY-MM-DD) |
:122 |
"kb-source-fs" |
Discovered event 의 hard-coded agent |
SQLite provenance_json |
:128 |
"kb-parse-md" |
Parsed event 의 hard-coded agent |
SQLite provenance_json |
:134 |
"kb-normalize" |
Normalized event 의 hard-coded agent |
SQLite provenance_json |
:143 |
warning_agent(&w.kind).to_string() → "kb-parse-md" (4 variant 모두) |
Warning event 의 agent |
SQLite provenance_json |
:153 |
"kb-normalize" |
lift_warnings loop 의 hard-coded agent (AudioRef-deferred) |
SQLite provenance_json |
- 5 위치 모두
ProvenanceEvent.agent: String필드로 누적. Provenance는serde_json::to_string(&doc.provenance)로 직렬화되어documents.provenance_jsonBLOB 컬럼에 persist (crates/kebab-store-sqlite/src/documents.rs:726).- wire export 경로 0: 어떤
--json출력에도provenancefield 가 노출되지 않음. SQLite-only storage = wire 외.tracing의 target literal 도 wire 외 (log file).
결론: warning_agent 의 return string ("kb-normalize" / "kb-parse-md") 은 wire-invisible (SQLite-internal). String 값을 변경해도 wire schema 영향 = 0. 단 DB compat 차원에서는 기존 row 의 string 값과 신규 row 의 값이 일치하면 audit log 가 일관 — § 3.7 의 callsite migration 에서 string 보존 정책 명시.
§2 Goals + non-goals
§2.1 Goals
- G1: 24 → 22 crate.
kebab-normalize+kebab-parse-types두 crate 흡수. - G2: design §3.7b 재작성 — "thin layer" 의 현재 무용성 + 보존된 3 forward-declared struct 의 future re-extraction trigger 명시.
- G3: design §8 graph 갱신 — 3 edge 제거 + 2 forbidden bullet 의미 갱신.
- G4: HOTFIXES.md 신규 entry (4-block 변형, design deviation Symptom).
- G5: 모든 referencing task spec (~25개) frozen 유지. HOTFIXES.md 가 live source of truth (CLAUDE.md 의 "Task specs themselves stay frozen" 룰).
- G6: wire schema 변경 0건. CLI / TUI / MCP surface 변경 0건.
- G7:
kebab-app의 deadkebab-parse-typesregular dep incidental cleanup. - G8: target_version = 0.19.0 (frozen design contract 변경 trigger).
- G9:
cargo tree -p kebab-app | grep -E "kebab_parse_types|kebab_normalize"= 0 줄 (post-absorb invariant).
§2.2 Non-goals
kebab-normalize의 lift 로직 의미 변경 (1:1 lift —build_canonical_documentbody 와derive_titlebody 가 byte-identical 하게 destination 으로 이동).- 5 사용 type (
ParsedBlock/ParsedBlockKind/ParsedPayload/Warning/WarningKind) 의 의미 변경 (1:1 이동, serde 표현 + variant 명 보존). - 3 forward-declared struct (
ParsedImageRegion/ParsedPdfPage/ParsedAudioSegment) 의 surface 변경 (보존, future surface 명시). - 4 parser 중 다른 parser (pdf / image / code) 가 normalize 를 신규로 거치도록 변경.
kebab-core의 도메인 타입 (Block,CanonicalDocument,Provenance, …) 변경.- ~25 referencing task spec 의 mechanical update (frozen 유지 — HOTFIXES live source 룰).
parser_versioncascade 변경 (lift 로직 보존이므로 cascade trigger 없음).- p9-fb-07 의
derive_titleAPI contract 변경 (call site 이동만, signature 보존). - README / HANDOFF / ARCHITECTURE 의 user-visible surface 변경 (단 ARCHITECTURE.md 의 crate graph 는 mechanical 갱신).
§3 Design
§3.1 Destination = Option A (kebab-parse-md 흡수)
흡수 대상 destination 으로 kebab-parse-md 단일 crate 를 선택 (team-lead 메시지 #2).
근거 (evidence-driven):
kebab-parse-types의 production caller =kebab-parse-md+kebab-normalize둘.kebab-normalize는 자신을 흡수당하는 입장 → 자연스러운 합류 지점 =kebab-parse-md.kebab-normalize::warning_agent의 분기 4건 중 4건 모두WarningKind::*의 emit 위치가kebab-parse-md(blocks.rs,frontmatter.rs). 즉 현실 의 emit-site 가 이미kebab-parse-md단일.- 4 parser 중 markdown 만 lift 를 거치므로, lift 가
kebab-parse-md안에 있는 것이 caller 일관성과 맞음.
대안 (Option B: kebab-app 흡수) 의 거부 이유:
kebab-app은 facade — store / embed / llm / parse 전반의 orchestration 책임. lift 같은 markdown-specific 도메인 코드가 들어가면 facade 의 단일책임 침해.kebab-app으로 흡수 시Vec<ParsedBlock>타입을 facade 가 노출하게 됨 → UI crate (cli/tui/mcp) 가 lift 의 intermediate type 을 indirect 으로 보게 되는 가능성.
대안 (Option C: kebab-core 흡수) 의 거부 이유:
kebab-core는 도메인 모델 only (Cargo.toml description). 8 variantParsedPayloadenum + markdown-specific Warning 류가 core 에 들어가면 §3.7b 가 명시한 namespace 폭발 우려가 정확히 현실화.
§3.2 Module placement
crates/kebab-parse-md/src/ 의 현 구성: lib.rs, blocks.rs, frontmatter.rs.
흡수 후 구성:
crates/kebab-parse-md/src/
├── lib.rs # 기존 + pub re-exports (5 사용 type + 3 보존 struct + 2 normalize fn)
├── blocks.rs # 기존 (use kebab_parse_types::* → use crate::types::* 갱신)
├── frontmatter.rs # 기존 (use kebab_parse_types::* → use crate::types::* 갱신)
├── types.rs # 신규 — kebab-parse-types/src/lib.rs 의 98 LOC 1:1 이식
└── normalize.rs # 신규 — kebab-normalize/src/lib.rs 의 production fn body 이식
근거:
types.rs+normalize.rs분리는 readability (lib.rs 가 thin re-export layer 로 유지).- 1:1 이식 = git blame / cargo doc / test grep 모두 file 단위로 traceable.
- 흡수 후에도 grep
crate/file단위로 "이 코드가 원래 어디서 왔는가" 추적 가능.
대안 거부: lib.rs 1 file 에 모두 합치는 옵션은 (a) blocks.rs + frontmatter.rs 의 use statement 가 use crate::* 로 광범위해지고, (b) lib.rs LOC 가 1200+ 으로 부풀어 review 부담. 분리 유지.
§3.3 Visibility 정책
destination = kebab-parse-md 의 lib.rs 가 다음을 re-export.
| symbol | 현 visibility | 흡수 후 visibility | 근거 |
|---|---|---|---|
ParsedBlock |
pub |
pub (re-export from types.rs) |
kebab-app 이 import 안 함이지만 future caller 대비 + tasks/p1-2.md (frozen) 가 pub API 로 freeze |
ParsedBlockKind |
pub |
pub |
동상 |
ParsedPayload |
pub |
pub |
동상 |
Warning |
pub |
pub |
동상 |
WarningKind |
pub |
pub |
동상 |
ParsedImageRegion |
pub |
pub (re-export, 보존 — §3.7b 재작성 후에도 surface) |
future re-extraction trigger 시 surface 가 stable 해야 함 |
ParsedPdfPage |
pub |
pub |
동상 |
ParsedAudioSegment |
pub |
pub |
동상 |
build_canonical_document |
pub |
pub (re-export from normalize.rs) |
kebab-app::lib.rs:51 의 single import |
derive_title |
pub |
pub |
tasks/p9/p9-fb-07-md-title-fallback.md (frozen) 의 API contract |
warning_agent |
fn (private) |
fn (pub(crate) — 동일 module 내 사용) |
wire-invisible 내부 helper |
kebab_core::{id_for_block, id_for_doc} re-export |
pub use |
(제거 — direct kebab_core::* 사용으로 통일. frozen p1-4 surface 의 re-export 후퇴이나 kebab-normalize::id_for_* 경유 production caller = 0 — 모든 caller 가 kebab_core::* 직접 import. 본 결정의 후퇴 위험 §6.10 R10 참조.) |
kebab-normalize 가 kebab-core re-export 한 패턴은 historical artifact. kebab-parse-md 가 직접 kebab-core import 하므로 re-export 필요 없음 |
중요: kebab-app::lib.rs:51 의 use kebab_normalize::build_canonical_document; 가 흡수 후 use kebab_parse_md::build_canonical_document; 로 갱신. signature byte-identical.
§3.4 Cargo.toml diff per crate
영향받는 Cargo.toml = 4 file (kebab-parse-md / kebab-normalize 삭제 / kebab-parse-types 삭제 / kebab-app).
crates/kebab-parse-md/Cargo.toml (변경):
[package]
name = "kebab-parse-md"
-description = "Markdown frontmatter and block parsing into kb-core::Metadata / kb-parse-types intermediates"
+description = "Markdown frontmatter + block parsing + canonical-document lift (absorbed kb-parse-types + kb-normalize, see HOTFIXES.md 2026-05-26)"
[dependencies]
kebab-core = { path = "../kebab-core" }
-kebab-parse-types = { path = "../kebab-parse-types" }
anyhow = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
time = { workspace = true }
tracing = { workspace = true }
+unicode-normalization = "0.1" # 흡수된 kb-normalize 의 NFKC 의존성
pulldown-cmark = { version = "0.13", default-features = false }
serde_yaml_ng = "0.10"
toml = "0.8"
lingua = { version = "1.8", default-features = false, features = [...] }
[dev-dependencies]
serde_json = { workspace = true }
+# 흡수된 kb-normalize 의 snapshot test 가 사용했던 dev-dep 들은 (kb-parse-md 가
+# 이미 자신을 통한 in-crate test 로 대체 가능하므로) 신규 추가 없음.
crates/kebab-normalize/Cargo.toml — 삭제 (디렉토리 전체 제거).
crates/kebab-parse-types/Cargo.toml — 삭제.
crates/kebab-app/Cargo.toml (변경):
[dependencies]
kebab-core = { path = "../kebab-core" }
kebab-config = { path = "../kebab-config" }
kebab-source-fs = { path = "../kebab-source-fs" }
kebab-parse-md = { path = "../kebab-parse-md" }
-kebab-parse-types = { path = "../kebab-parse-types" }
-kebab-normalize = { path = "../kebab-normalize" }
kebab-chunk = { path = "../kebab-chunk" }
...
→ kebab-parse-types (dead dep) 와 kebab-normalize (live dep, 흡수됨) 둘 다 제거. kebab-parse-md 가 (re-export 통해) 두 crate 의 surface 를 모두 제공.
crates/kebab-chunk/Cargo.toml (변경):
[dev-dependencies]
# kb-parse-md / kb-normalize / kb-parse-code are dev-only — used by the
# snapshot integration tests to build a CanonicalDocument from fixture files.
# Forbidden as regular deps per design §8 (chunker consumes CanonicalDocument
# from kb-core only); `cargo tree -p kb-chunk --depth 1` (default scope,
# excludes dev-deps) confirms this.
kebab-parse-md = { path = "../kebab-parse-md" }
kebab-parse-code = { path = "../kebab-parse-code" }
-kebab-normalize = { path = "../kebab-normalize" }
serde_json = { workspace = true }
time = { workspace = true }
→ snapshot integration test 의 fixture builder 가 kebab-normalize::build_canonical_document 를 호출했었음 → kebab-parse-md::build_canonical_document 로 갈음 (이미 dev-dep 으로 존재 — 신규 add 0). 통합 test source (tests/*.rs) 의 use kebab_normalize::*; → use kebab_parse_md::*; 갱신.
crates/kebab-store-sqlite/Cargo.toml (변경):
[dev-dependencies]
tempfile = "3"
serde_json = { workspace = true }
# kb-parse-md / kb-normalize / kb-chunk are dev-only — used to build a
# CanonicalDocument + Vec<Chunk> from a fixture in the contract round-trip
# test. Forbidden as regular deps per design §8 (store consumes domain
# types from kb-core only); `cargo tree -p kb-store-sqlite --depth 1`
# (default scope, excludes dev-deps) confirms this.
kebab-parse-md = { path = "../kebab-parse-md" }
-kebab-normalize = { path = "../kebab-normalize" }
kebab-chunk = { path = "../kebab-chunk" }
→ contract round-trip test 의 use kebab_normalize::*; → use kebab_parse_md::*; 갱신. 위와 동일 패턴.
dev-dep migration 의 evidence 명령 (verification 시 사용):
# 흡수 전 (현 시점) — kebab-normalize regular + dev-dep 모두 hit:
$ grep -l "kebab-normalize" crates/*/Cargo.toml
crates/kebab-app/Cargo.toml # regular, §3.4 의 kebab-app 변경에서 제거
crates/kebab-chunk/Cargo.toml # dev-only, 위 diff 에서 제거
crates/kebab-normalize/Cargo.toml # 본 PR 에서 디렉토리 자체 삭제
crates/kebab-store-sqlite/Cargo.toml # dev-only, 위 diff 에서 제거
# 흡수 후 (post-PR head) — expected:
$ grep -l "kebab-normalize" crates/*/Cargo.toml
(0 line)
§3.5 design §3.7b 재작성 — 정확한 wording
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md:703-764 의 §3.7b 를 다음으로 교체.
Before (line 703-764, 약 60 LOC): 본 spec §1.7 + §1.6 인용 — "thin layer 가 raison d'être" 의 정당화.
After (제안):
### 3.7b Parser intermediate types — `kebab-parse-md` 흡수 후 (post-v0.19.0)
**원래 의도**: parser 의 *중간* 표현 (`ParsedBlock` 류) 을 `kebab-core` 가 아닌 별도 thin crate `kebab-parse-types` 에 두고, `kebab-normalize` 가 medium-agnostic 한 ID/Provenance lift 책임을 가지는 layered 구조 (v0.1~v0.18 머지 시점의 초기 design).
**현재 상태 (v0.19.0~)**: `kebab-parse-types` 와 `kebab-normalize` 두 crate 가 `kebab-parse-md` 에 흡수됨. 근거:
- 4 parser (`kebab-parse-md` / `kebab-parse-pdf` / `kebab-parse-image` / `kebab-parse-code`) 중 `kebab-parse-md` 한 갈래만 `kebab-normalize` 를 경유. 나머지 3 parser 는 `CanonicalDocument` 를 직접 emit — thin layer 의 fan-in/fan-out 모두 1.
- production caller 가 1개로 collapse 되어 layer 가 의미 잃음.
- 본 흡수 의 audit log = `tasks/HOTFIXES.md` 의 dated entry (2026-05-26 — "design deviation").
**보존된 surface**: `ParsedBlock`, `ParsedBlockKind`, `ParsedPayload`, `Warning`, `WarningKind`, 그리고 3 forward-declared struct (`ParsedImageRegion`, `ParsedPdfPage`, `ParsedAudioSegment`) 는 `kebab-parse-md` 의 `pub` re-export 로 보존. 의미와 serde 표현 모두 byte-identical.
**future re-extraction trigger** (측정 시점 명시 — `build_canonical_document` 의 input variant 변경 지점): 다음 중 하나가 발생하면 layer 재추출 (별 spec + 별 PR). §11 의 trigger 조건과 일관 cross-link (본 §3.5 의 list 와 §11.6 의 list 는 동일 의미).
1. `kebab-parse-pdf` / `kebab-parse-image` / `kebab-parse-audio` (audio 는 **P8 도입 시** — 현재 deferred, `tasks/INDEX.md` 의 Phase 8 row 참조) 가 `ParsedBlock` 또는 그 변종 (`ParsedPdfPage`, `ParsedImageRegion`, `ParsedAudioSegment`) 를 emit 시작 + `kebab-normalize` 의 lift 를 경유하도록 변경. **측정**: `kebab_parse_md::build_canonical_document` 의 input variant 가 `Vec<ParsedBlock>` 외 medium 의 변종이 추가되는 시점.
2. 즉, fan-in ≥ 2 (parser caller 2개 이상) 가 회복.
3. 또는 lift 로직이 markdown-only specific 함수에서 medium-agnostic 함수로 일반화 필요.
위 trigger 발생 전까지는 `kebab-parse-md` 내부의 `types.rs` + `normalize.rs` module 로 유지.
**의존 그래프 (post-absorb)**:
```text
kebab-core (도메인 모델 — Block, Chunk, SourceSpan, IDs, …)
▲
│
kebab-parse-md (markdown 의 frontmatter + block + types + normalize, 모두 in-crate)
▲
│
kebab-parse-pdf, kebab-parse-image, kebab-parse-code (자체 CanonicalDocument emit)
kebab-parse-md 는:
kebab-core에만 의존 (Block,SourceSpan,Lang등 도메인 타입 사용).- 다른 어떤
kebab-*에도 의존하지 않는다. - parser 구체 라이브러리 (
pulldown-cmark) 와 normalize helper (unicode-normalization) 에 의존.
보존된 surface (계속): 5 사용 type 의 정의 (ParsedBlock 의 4 field + ParsedBlockKind 의 8 variant + ParsedPayload 의 8 variant + Warning + WarningKind 의 4 variant) 와 3 forward-declared struct 의 본문은 P1 spec 의 원본 보존 — wire 표현 (serde rename_all / tag) 변경 0.
Tracing instrumentation policy: actual crates/kebab-normalize/src/lib.rs:109 에 explicit literal target: "kebab-normalize" 가 hard-coded (자동 module-path derive 아님). 흡수 후 manual 1-line 갱신 필요. stage label 보존 정책 (warning_agent 보존과 일관) 시 target: "kebab-normalize" 유지 — 호환되는 log scraper grep 일관성. 명시적 갱신 원할 시 target: "kebab-parse-md::normalize". 본 spec 의 권장 = 보존 (stage label = "kebab-normalize" — 흡수 후에도 lift stage 의 의미 보존 + R8 mitigation). §3.7 callsite migration 의 (g) 에 1-line touch site 명시. wire / surface impact 0 (§6.8 R8 cross-link).
### §3.6 design §8 graph diff
`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md:1457-1491` 의 §8 graph 를 다음으로 **교체**.
**Before (line 1457-1491, 약 35 LOC)**: 본 spec §1.7 의 evidence chain 에서 인용.
**After (제안 — line 단위 diff)**:
```diff
```text
kebab-cli, kebab-tui, kebab-desktop
└─> kebab-app
├─> kebab-source-fs
│ (p10-2 이후: lang detect + skip policy 내장; kebab-parse-code 와 분리)
- ├─> kebab-parse-md / kebab-parse-pdf / kebab-parse-image / kebab-parse-audio
- │ └─> kebab-parse-types (parser intermediate)
+ ├─> kebab-parse-md
+ │ (post-v0.19.0: absorbed kebab-parse-types + kebab-normalize — §3.7b)
+ ├─> kebab-parse-pdf / kebab-parse-image (self-emit CanonicalDocument)
├─> kebab-parse-code
│ └─> kebab-core (domain types only — NO store/embed/llm/rag/UI)
- ├─> kebab-normalize
- │ └─> kebab-parse-types
├─> kebab-chunk
├─> kebab-store-sqlite (DocumentStore, JobRepo, Retriever[lexical])
├─> kebab-store-vector (VectorStore)
├─> kebab-embed-local
├─> kebab-search (Retriever[hybrid])
├─> kebab-llm-local
├─> kebab-rag
├─> kebab-eval
└─> kebab-config
└─> kebab-core (모두 의존)
-kebab-parse-types 는 kebab-core 와 parsers/normalize 사이의 thin layer (§3.7b 참조). parser-별 중간 표현 (ParsedBlock, ParsedImageRegion, ParsedPdfPage, ParsedAudioSegment, Inline) 을 한 곳에 모아 (a) kebab-core 의 namespace 폭발을 막고 (b) kebab-normalize 가 parser 를 직접 import 하지 않게 한다.
+kebab-parse-md 는 v0.19.0 부터 kebab-parse-types (parser intermediate types) 와 kebab-normalize (CanonicalDocument lift) 를 흡수한다 (§3.7b 참조). 4 parser 중 markdown 한 갈래만 lift 를 경유하므로 thin layer 의 가치가 의미를 잃었다. 보존된 5 사용 type + 3 forward-declared struct 의 surface 는 kebab-parse-md 의 pub re-export 로 backward-compat.
핵심 금지:
- UI → store/llm/parse 직접 의존 ✗
- parse-* → store/llm/embed ✗ -- parse-* → kebab-normalize ✗ (단방향: parsers → kebab-parse-types ← normalize) +- parse-* (pdf/image/code) → kebab-parse-md ✗ (parser 끼리 cross-import 금지 — markdown 의 lift 가 다른 parser 에 노출되면 안 됨)
- chunk → llm/embed ✗
-- normalize → store / parse-* ✗
-- kebab-parse-types → 어떤 parser/normalize/store/llm/embed/search/rag/ui ✗ (
kebab-core만 의존) - 다른 store 와 cross-write ✗
**중요**:
- 새로운 forbidden bullet "parse-* (pdf/image/code) → kebab-parse-md ✗" 는 흡수 부산물 — `kebab-parse-md` 가 normalize 까지 흡수했으므로, 만약 다른 parser 가 `kebab-parse-md` 를 import 하면 lift 를 indirect 으로 사용하게 됨 (그리고 §3.7b 의 future re-extraction trigger 가 정확히 발동). 본 forbidden bullet 이 그 invariant 를 명시.
- *MAJOR #5 의 critic 의견에 따라* "kebab-parse-md → store / llm / embed ✗" 명시 룰은 추가하지 않음 — 기존 `parse-* → store/llm/embed ✗` 룰이 흡수된 lift 까지 자동 포함 (parse-md 도 parse-* 의 한 갈래). design contract 중복 룰 방지.
### §3.7 callsite migration
**(a) `kebab-app/src/lib.rs:51`**:
```diff
-use kebab_normalize::build_canonical_document;
+use kebab_parse_md::build_canonical_document;
(b) kebab-app/src/lib.rs:1119 의 context string (byte-identical 한 hunk 는 모두 삭제 — kb-parse-md::parse_frontmatter 의 line 1091 / kb-parse-md::parse_blocks 의 line 1099 는 변경 0):
@@ crates/kebab-app/src/lib.rs:1119 @@
- .context("kb-normalize::build_canonical_document")?;
+ .context("kb-parse-md::build_canonical_document")?; // 흡수 후 callsite 명시
(c) kebab-parse-md/src/blocks.rs, crates/kebab-parse-md/src/frontmatter.rs:
-use kebab_parse_types::{ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind};
+use crate::types::{ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind};
(d) kebab-parse-md/src/normalize.rs (신규 — 이식된 body):
-use kebab_parse_types::{ParsedBlock, Warning, WarningKind};
+use crate::types::{ParsedBlock, Warning, WarningKind};
-pub use kebab_core::{id_for_block, id_for_doc};
+// (re-export 제거 — kebab-parse-md 가 이미 kebab-core 의존, 호출자가 직접 import)
(e) warning_agent + hard-coded agent string 정책 (§1.9 의 wire-invisibility 검증 결과 기반).
actual code 의 정확한 distinction:
-
warning_agent의 body: 4WarningKindvariant 모두"kb-parse-md"단일 return —warning_agent자체는 "kb-normalize" string 을 반환 안 함 (crates/kebab-normalize/src/lib.rs:187-191). -
별도 hard-coded
"kb-normalize"literal 2 곳 (warning_agent 와 별개):lib.rs:134—Normalizedevent 의agentfield (build_canonical_document body 의 lift 종료 시점 ProvenanceEvent push).lib.rs:153—lift_warningsloop 의 agent field (lift-stage warning 의 attribution — 현재 AudioRef-deferred drops only).
-
또 다른 hard-coded agent literal 2 곳 (보존 대상, 흡수 후 의미 변경 없음):
lib.rs:122—"kb-source-fs"(Discovered event — kebab-source-fs 의 stage label, 흡수와 무관).lib.rs:128—"kb-parse-md"(Parsed event — markdown parse stage label).
// crates/kebab-parse-md/src/normalize.rs (이식 후 — 정확한 보존 정책)
//
// IMPORTANT: 다음 4 hard-coded agent literal 은 SQLite 의 documents.provenance_json
// BLOB 으로만 persist (wire-invisible). 흡수 후에도 모두 보존 — stage label
// 의미가 crate 흡수와 독립 (stage 자체는 변하지 않음).
//
// line 122: "kb-source-fs" (Discovered event) — 변경 X
// line 128: "kb-parse-md" (Parsed event) — 변경 X
// line 134: "kb-normalize" (Normalized event) — 변경 X (★ 본 PR 보존 결정)
// line 153: "kb-normalize" (lift_warnings event) — 변경 X (★ 본 PR 보존 결정)
//
// warning_agent 자체는 4 WarningKind variant 모두 "kb-parse-md" 단일 return
// (lib.rs:187-191). String 값을 변경하지 않음 — old DB row 의 audit log
// (예: "kb-normalize") 와 new DB row 의 값이 일치하여 history grep
// (`SELECT provenance_json FROM documents WHERE provenance_json LIKE '%kb-normalize%'`)
// 결과의 의미적 일관성이 보존된다.
fn warning_agent(kind: &WarningKind) -> &'static str {
match kind {
WarningKind::MalformedFrontmatter | WarningKind::EncodingFallback => "kb-parse-md",
WarningKind::MalformedTable => "kb-parse-md",
WarningKind::ExtractFailed => "kb-parse-md",
}
}
(g) tracing::debug! target literal (crates/kebab-normalize/src/lib.rs:109):
// 본 PR 결정 = 보존 (stage label 일관성 — warning_agent 와 같은 정책).
tracing::debug!(
target: "kebab-normalize", // ← 흡수 후에도 변경 X
"built canonical document doc_id={} blocks={}",
doc_id.0,
lifted_blocks.len()
);
명시적 갱신 원할 시 → target: "kebab-parse-md::normalize" 한 줄 변경. 본 spec 권장 = 보존 — ~/.local/state/kebab/logs/kb.log.YYYY-MM-DD 의 기존 grep pattern 일관성. R8 mitigation 의 핵심.
(f) test file 이동:
-crates/kebab-normalize/tests/normalize_snapshot.rs
+crates/kebab-parse-md/tests/normalize_snapshot.rs
→ destination 이 kebab-parse-md 이므로 (이미 kebab-parse-md 를 dev-dep 로 사용했던) integration test 가 자기 자신 crate 의 test 로 collapse. Cargo.toml 의 kebab-parse-md = { path = "..." } dev-dep declare 가 자기 참조 → 제거 (cargo standard behavior 명시: tests/*.rs integration test 는 자기 crate 의 lib 를 자동 link, dev-dep declare 불필요 — cargo test -p kebab-parse-md --test normalize_snapshot 가 갱신 후에도 green. R3/Q4 closure).
§3.8 Cargo workspace.members 갱신
본 변경 은 Cargo.toml 의 두 분리된 block 에 영향 — plan/executor 가 한 hunk 로 적용 시 line-context mismatch 가능. 두 hunk 로 분리 (NEW NIT #N6 closure):
Hunk (a) — [workspace] members 의 두 entry 삭제:
[workspace]
resolver = "3"
members = [
"crates/kebab-core",
- "crates/kebab-parse-types",
"crates/kebab-config",
"crates/kebab-source-fs",
"crates/kebab-parse-md",
- "crates/kebab-normalize",
"crates/kebab-chunk",
...
]
Hunk (b) — [workspace.package] version 의 1-line 변경:
[workspace.package]
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
repository = "https://github.com/altair823/kebab"
-version = "0.18.0"
+version = "0.19.0" # frozen design contract (§3.7b 재작성) 변경 trigger — CLAUDE.md "Release / binary version bump"
→ count: 24 → 22. Cargo.lock auto-갱신 (§5.11 verification).
§3.9 HOTFIXES.md entry — 정확한 wording
tasks/HOTFIXES.md 의 가장 최근 entry 위 (chronological reverse) 에 다음 4-block 변형을 추가.
## 2026-05-26 — design deviation — kebab-normalize + kebab-parse-types 흡수 (24 → 22 crates)
**Symptom**: design deviation — post-PR9 audit (system-architect, `tasks/INDEX.md` L169) identified 두 crate (`kebab-normalize` + `kebab-parse-types`) 가 dead abstraction. design §3.7b 의 "thin layer" raison d'être ((a) `kebab-core` namespace 폭발 방지, (b) normalize 의 parser non-dependence) 가 4 parser 중 1개 (markdown) 만 lift 를 경유하는 현실에서 fan-in/fan-out 모두 1 → layer 의미 잃음. `kebab-parse-types` 의 production caller 가 2개 (`kebab-parse-md` + `kebab-normalize`) 이고 `kebab-normalize` 자체 caller 가 1개 (`kebab-app`) — 모두 markdown 의 lift 경로 안에서 단일 fan-in 경계 가능.
**Root cause**: P1~P10 머지를 거치며 `kebab-parse-pdf` (P7) / `kebab-parse-image` (P6) / `kebab-parse-code` (P10) 가 `CanonicalDocument` 직접 emit 패턴으로 정착. `kebab-normalize::build_canonical_document` 는 markdown-specific `Vec<ParsedBlock>` → `CanonicalDocument` lift 만 책임. design §3.7b 가 가정한 "ParsedBlock 류는 모든 parser 가 emit → normalize 가 일괄 lift" 의 fan-in ≥ 2 시나리오가 미도래 — 그러나 layer 비용 (24 crate workspace, 두 crate 의 lib.rs only structure) 은 계속 지불.
**Action**: `kebab-normalize` (1097 LOC) + `kebab-parse-types` (98 LOC) 를 `kebab-parse-md` 에 흡수 — 22 crate workspace.
- `crates/kebab-parse-md/src/types.rs` (신규): `kebab-parse-types/src/lib.rs` 의 98 LOC 1:1 이식 (5 사용 type + 3 forward-declared struct 보존).
- `crates/kebab-parse-md/src/normalize.rs` (신규): `kebab-normalize/src/lib.rs` 의 production fn body (`build_canonical_document`, `derive_title`, `warning_agent`) 이식. `warning_agent` 의 return string ("kb-normalize") 보존 — SQLite `documents.provenance_json` 의 audit log 일관성 (wire-invisible, see §1.9).
- 3 dead struct (`ParsedImageRegion` / `ParsedPdfPage` / `ParsedAudioSegment`) 는 보존 — v0.20+ image/pdf normalize integration 의 future surface (본 spec §11 참조).
- `crates/kebab-parse-md/src/lib.rs`: `pub use crate::types::*; pub use crate::normalize::{build_canonical_document, derive_title};` re-export 추가.
- `crates/kebab-parse-md/src/{blocks,frontmatter}.rs`: `use kebab_parse_types::*` → `use crate::types::*`.
- `crates/kebab-app/src/lib.rs:51`: `use kebab_normalize::build_canonical_document` → `use kebab_parse_md::build_canonical_document`.
- `crates/kebab-app/Cargo.toml`: `kebab-normalize` regular dep 제거 + `kebab-parse-types` regular dep 제거 (후자는 dead dep — `cargo tree -p kebab-app | grep kebab_parse_types` 0줄 검증으로 incidental cleanup).
- `Cargo.toml` workspace.members: `kebab-normalize` + `kebab-parse-types` entries 제거. `workspace.package.version` 0.18.0 → **0.19.0** (frozen design contract 변경 trigger — CLAUDE.md "Release / binary version bump").
- `crates/kebab-normalize/` + `crates/kebab-parse-types/` 디렉토리 전체 삭제 (`git rm -r`).
- `crates/kebab-normalize/tests/normalize_snapshot.rs` → `crates/kebab-parse-md/tests/normalize_snapshot.rs` (mechanical move, `Cargo.toml` 자기 참조 dev-dep 제거).
- `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §3.7b 재작성 (보존 + future re-extraction trigger 명시) + §8 graph 갱신 (3 edge 제거 + 2 forbidden bullet 의미 갱신, "parse-* → kebab-normalize ✗" 룰 의미 부분 폐기).
- `docs/ARCHITECTURE.md` crate graph + 디렉토리 tree mechanical 갱신.
- `tasks/INDEX.md` L169 의 "kebab-normalize 흡수" defer mention 해소.
**Amends**: spec `docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md` cross-link. design `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §3.7b + §8 동시 갱신 (CLAUDE.md "Changing the design doc requires updating every referencing task spec in the same PR" — 본 PR 의 design 갱신은 ~25 referencing task spec 의 raison d'être 인용을 stale 화하지만, frozen 원칙에 따라 mechanical update 없음. live source of truth = 본 HOTFIXES entry). 영향받는 task spec 의 `Forbidden dependencies` 또는 `contract_sections: ["§3.7b"]` 인용은 historical contract 로 보존됨 — `tasks/p1/p1-2-parser-types.md`, `tasks/p1/p1-3-markdown-parser.md`, `tasks/p1/p1-4-normalize.md`, `tasks/p9/p9-fb-07-md-title-fallback.md` 등. (Wire / surface impact: 0건 — CLI / TUI / MCP / `--json` 출력 / config / XDG path / parser_version 모두 unchanged. wire-invisible `provenance.events[].agent` 의 stage label "kb-normalize" 도 보존 — old DB row 와 new DB row 의 audit log 일관성.)
§3.10 kebab-app 의 dead kebab-parse-types regular dep incidental cleanup
본 PR 에 같이 묶이는 이유 (team-lead 결정 #4):
kebab-app/Cargo.toml:15의kebab-parse-types = { path = "../kebab-parse-types" }regular dep declare.grep -rn "kebab_parse_types" crates/kebab-app/src/: 0 hit.- 즉
kebab-app은kebab-parse-types의 production caller 가 아님 (analyst evidence). - 본 PR 이
kebab-parse-types를 삭제하면 declare 도 어차피 제거 필요 → incidental cleanup. 별 PR 불필요.
Verification: cargo build -p kebab-app 가 dead dep 제거 후에도 green. cargo tree -p kebab-app | grep kebab_parse_types = 0줄.
§4 Open questions
§4.1 Resolved (사용자 결정 + analyst evidence)
- target_version 0.19.0 vs 0.18.1: → 0.19.0 (frozen design contract 변경, CLAUDE.md release rule). [resolved by user]
- Destination = parse-md vs app vs core: → parse-md (caller 일관성 + 단일책임). [resolved by user + §3.1 evidence]
- 3 forward-declared struct 처리: → 보존 + future surface 명시 (re-extraction trigger). [resolved by user, §3.3 + §3.5]
- ~25 referencing task spec mechanical update: → 0건 (frozen 룰). [resolved by user]
- HOTFIXES entry format: → 4-block 변형 ("design deviation" Symptom). [resolved by user + §3.9]
- kebab-app 의 dead parse-types dep: → 같은 PR incidental cleanup. [resolved by user + §3.10]
- warning_agent wire visibility: → wire-invisible (§1.9 verified, 16 wire schema 모두 0 hit — search_response.schema.json:35 의 description 산문 false-positive 명시). String 값 보존 정책 (§3.7e). [resolved by planner verification]
§4.2 Unresolved (critic round 에서 결정)
-
Q1 —
warning_agent의 return string 정책: 보존 ("kb-normalize"유지) vs 갱신 ("kb-parse-md"단일화)?- 보존 근거: SQLite 의 audit log 일관성 (old + new DB row 의 grep 의미 보존), stage label 의미 (lift stage ≠ crate name).
- 갱신 근거: crate name 과 일치하여 newcomer 혼란 감소.
- 본 spec 의 현재 권장 = 보존 (§3.7e). critic round 에서 재확정.
-
Q2 —
kebab-parse-md의 description string 의 정확한 wording:- 현재:
"Markdown frontmatter and block parsing into kb-core::Metadata / kb-parse-types intermediates". - 흡수 후 후보:
"Markdown frontmatter + block parsing + canonical-document lift (absorbed kb-parse-types + kb-normalize, see HOTFIXES.md 2026-05-26)". - HOTFIXES cross-link 을 description string 에 두는 것이 적절한지 critic 의견.
- 현재:
-
Q3 —
kebab-parse-md의 lib.rs re-export 가pub use crate::types::*; pub use crate::normalize::{build_canonical_document, derive_title};glob + specific 혼용:- 5 type + 3 struct 의 glob 가 future addition 의 surface leak 위험.
- 대안: 8 type 모두 explicit re-export (
pub use crate::types::{ParsedBlock, ParsedBlockKind, ..., ParsedAudioSegment};). - critic 의견.
-
Q4 —
kebab-parse-md의 dev-dep 정리: 흡수된kebab-normalize/tests/normalize_snapshot.rs가 이전에는kebab-parse-md를 dev-dep 으로 사용해서 fixture 를 만들었음 (crates/kebab-normalize/Cargo.toml [dev-dependencies] kebab-parse-md). 흡수 후 자기 자신을 dev-dep 으로 declare 할 필요 없음 (cargo가 자기 crate test 자동 link). cargo 가 어떻게 처리하는지 별 verification 필요한지?- 본 spec 의 현재 권장 = 자기 참조 dev-dep declare 제거 (in-crate integration test 는
tests/*.rs가use kebab_parse_md::*;로 직접 link). critic round verification 으로 cargo 동작 확인 필요.
- 본 spec 의 현재 권장 = 자기 참조 dev-dep declare 제거 (in-crate integration test 는
-
Q5 — kebab-chunk + kebab-store-sqlite 의
kebab-normalizedev-dep:grep -l "kebab-normalize" crates/{kebab-chunk,kebab-store-sqlite}/Cargo.toml— 본 spec 에서 정확히 검증 필요. dev-dep 가 있으면kebab-parse-md로 갈음.- 본 spec 의 현재 권장 = §5.3 verification step 에서 mechanical 갱신 + critic 검토.
§4.3 Open questions log
본 spec 의 Q1~Q5 + critic round 에서 추가될 항목들은 critic round 종료 시 tasks/HOTFIXES.md 또는 followup spec 으로 closure.
§5 Verification plan
§5.1 Unit + integration test 회귀
cargo test --workspace --no-fail-fast -j 1 (CLAUDE.md 의 "-j 1 for the full workspace test isn't optional" 룰).
- Baseline: PR-9d 머지 시점 1313 tests (analyst evidence chain 의 baseline).
- Expected post-absorb: 1313 - X (kebab-normalize + kebab-parse-types 의 test 수) + X (destination 으로 이동된 동일 수). net delta = 0.
- 단, 자기 참조 dev-dep 제거로 통합되는 test scope 변경 가능 — 본 spec 작성 시점 baseline N 정확히 계측 → plan 단계에서 채움.
- 본 spec 의 약속: net delta = 0 또는 +N(신규 검증 test 의 의도된 addition).
§5.2 Workspace ground truth invariants
다음 invariant 가 PR head 에서 모두 green:
| invariant | 확인 명령 | expected |
|---|---|---|
| 22 crate workspace | cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length' (또는 ls -d crates/*/ | wc -l) — Cargo.toml 의 comment / whitespace 무관 robust |
22 |
kebab-normalize 디렉토리 부재 |
ls crates/kebab-normalize/ 2>&1 |
"No such" |
kebab-parse-types 디렉토리 부재 |
ls crates/kebab-parse-types/ 2>&1 |
"No such" |
kebab-app 의 dep tree |
cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)" |
0 줄 |
kebab-app 의 source import |
grep -rn "kebab_normalize|kebab_parse_types" crates/kebab-app/src/ |
0 hit |
| 5 사용 type 의 surface 보존 | cargo doc -p kebab-parse-md --no-deps + cat target/doc/kebab_parse_md/index.html | grep -c "ParsedBlock|ParsedPayload|Warning" |
≥ 5 |
| 3 forward-declared struct 의 surface 보존 | (위 doc grep) | ≥ 3 |
| clippy gate | cargo clippy --workspace --all-targets -- -D warnings |
0 warning |
§5.3 Cargo.toml 의 dev-deps grep + mechanical migration
흡수 전:
$ grep -l "kebab-normalize" crates/*/Cargo.toml
crates/kebab-app/Cargo.toml
crates/kebab-chunk/Cargo.toml
crates/kebab-normalize/Cargo.toml
crates/kebab-store-sqlite/Cargo.toml
흡수 후 expected:
$ grep -l "kebab-normalize" crates/*/Cargo.toml
(0 line — kebab-app: 본 PR 에서 제거. kebab-chunk + kebab-store-sqlite: dev-dep 가 있다면 kebab-parse-md 로 갈음. kebab-normalize: 디렉토리 자체 삭제)
kebab-parse-types:
$ grep -l "kebab-parse-types" crates/*/Cargo.toml
(0 line — kebab-app: 본 PR 에서 dead dep 제거. kebab-parse-md + kebab-normalize: 본 PR 에서 흡수 / 삭제. kebab-parse-types: 디렉토리 자체 삭제)
§5.4 Wire schema 회귀
docs/wire-schema/v1/*.json16 파일 모두 변경 0.git diff main..HEAD -- docs/wire-schema/v1/ | wc -l= 0.provenance.events[].agent의 stage label "kb-normalize" 보존 (§3.7e) — old DB 의 audit log 와 일관성.
§5.5 SMOKE 회귀
docs/SMOKE.md 가 정의하는 isolated TempDir KB pipeline (--config /tmp/kebab-smoke/config.toml) 의 ingest + search + ask 가 흡수 전후 byte-identical wire 출력. plan 단계에서 dogfood snapshot 비교.
§5.6 design doc 갱신 검증
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md§3.7b (line 703-764) 가 §3.5 의 wording 으로 교체.- 동일 doc §8 (line 1457-1491) 이 §3.6 의 diff 로 갱신.
git diff main..HEAD -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md의 hunk 가 위 2 section 만 touch.
§5.7 referencing task spec 의 frozen 검증
grep -rln "kebab-parse-types\|kebab-normalize\|§3.7b" tasks/ 의 hit 가 모두 본 PR 의 diff 에서 0 hit (frozen 룰).
본 spec 의 §7 가 명시한 4 frozen task spec (p1-2, p1-3, p1-4, p9-fb-07) + 다른 ~25 referencing task spec 모두 mechanical update 없음.
§5.8 ARCHITECTURE.md + INDEX.md 갱신
docs/ARCHITECTURE.md의 crate graph + 디렉토리 tree 갱신 — 24 → 22 crate, parse-types + normalize 절 삭제. mechanical.tasks/INDEX.mdL169 의 "kebab-normalize 흡수, … v0.18.1+ defer" 의 mention 갱신 — "(v0.19.0 closure — see HOTFIXES.md 2026-05-26)" cross-link 추가.tasks/INDEX.md의 "Future work / deferred" 섹션 (없으면 신설) 에 §11.7 의 한 줄 entry 추가.
§5.9 cargo deny workspace dep validation
# workspace 의 dep 룰 (license + advisories + ban + sources) 검증.
# 흡수된 두 crate 의 directory 삭제 후에도 ban 룰 (예: duplicate crate 금지)
# 위반 없어야 함.
$ cargo deny check
ok 0 errors, 0 warnings
§5.10 target/ 산출물 cleanup (CLAUDE.md 룰)
흡수된 두 crate 의 target/debug/deps/libkebab_normalize-*.rlib / libkebab_parse_types-*.rlib 가 stale artifact 로 남으면 build cache pollution. PR head 의 verification 직전 cargo clean 한 번 실행 — CLAUDE.md 의 "Run cargo clean routinely after each merged PR" 룰. (16 GB RAM 머신 의 link step 압박 회피 — §6.7 R7).
$ cargo clean
$ cargo test --workspace --no-fail-fast -j 1 # full re-build + test
§5.11 Cargo.lock 변경 검증
# kebab-normalize / kebab-parse-types 의 [[package]] entry 가 삭제.
$ grep '^name = "kebab-normalize"\|^name = "kebab-parse-types"' Cargo.lock
(0 hit)
# kebab-parse-md 의 dependencies 에 unicode-normalization 추가.
$ awk '/^\[\[package\]\]/,/^$/{if(/name = "kebab-parse-md"/)f=1; if(f) print; if(/^$/ && f){f=0; print "---"}}' Cargo.lock | grep "unicode-normalization"
unicode-normalization
§6 Risks
§6.1 R1 — §3.7b strike 의 stakeholder impact
위험: design contract 의 §3.7b 가 P1~P10 의 ~25 task spec 의 contract_sections 또는 Forbidden dependencies 에서 인용됨. frozen 룰에 따라 mechanical update 안 함 → reading these specs in isolation 시 stale 한 raison d'être 인용 노출.
Mitigation:
- HOTFIXES.md 의 dated entry 가 live source of truth (CLAUDE.md "Live deviations from the original contract go in
tasks/HOTFIXES.md" 룰). - design §3.7b 재작성 가 "원래 의도 / 현재 상태 / 보존된 surface / future re-extraction trigger" 4 단락 구조로, original intent + 현재 reality + future contingency 모두 명시.
- 본 spec 의 §3.5 wording 이 §3.7b 의 원래 paragraph 를 인용한 task spec 도 의미적 backward-compat (보존된 surface + future trigger 가 명시되어 원래 intent 의 일부 보존).
§6.2 R2 — warning_agent string 정책 (Q1) 의 audit log 일관성
위험: 흡수 후 "kb-normalize" 문자열을 그대로 emit 하면 newcomer 가 "이 crate 가 어디 갔지?" 혼란. 반대로 "kb-parse-md" 로 갈음하면 old DB 와 new DB 의 grep 결과 diverge.
Mitigation:
- §3.7e 의 권장 (보존) 가 stage label vs crate label 의 의미 분리 강조. comment 로 rationale inline.
- HOTFIXES entry 가 dated audit log — newcomer 의 첫 grep 결과가
tasks/HOTFIXES.md의 2026-05-26 entry 로 land.
§6.3 R3 — 자기 참조 dev-dep (Q4) 의 cargo 동작
위험: kebab-normalize/tests/normalize_snapshot.rs 가 kebab-parse-md 를 fixture builder 로 사용. 흡수되어 kebab-parse-md 안으로 이식되면 자기 참조 dev-dep 가 됨. cargo 가 이를 자동 link 하는 패턴 vs declare 필요 패턴의 분기.
Mitigation:
- plan 단계에서 cargo behavior 검증 (소규모 sandbox 또는
cargo test -p kebab-parse-md --test normalize_snapshot의 dry run). - 본 spec 의 §3.7f 가 "자기 참조 dev-dep declare 제거" 권장 — in-crate integration test 는
tests/*.rs가use kebab_parse_md::*;로 직접 link. cargo 의 standard behavior.
§6.4 R4 — future re-extraction trigger 의 비용 추정
위험: 흡수 후 kebab-parse-pdf 또는 kebab-parse-image 가 향후 ParsedBlock 을 emit 하도록 변경되면 (§3.5 의 trigger), layer 재추출 필요. 그 비용이 본 흡수 비용보다 클 위험.
Mitigation:
- 본 spec 의 §3.5 wording 이 trigger 1~3 을 명시 → future audit 시 명확.
- 1:1 이식 (types.rs + normalize.rs 분리 구조) 가 재추출 시 cherry-pick 용이.
- 그러나 ParsedBlock 의 emit-site 가 markdown 1개 → 2개로 확장될 조짐이 v0.19.0 시점에 0 (P8 audio 도 deferred). 본 위험의 발현 확률 = low.
§6.5 R5 — 5 사용 type 의 visibility 후퇴
위험: 5 type (ParsedBlock 등) 이 pub re-export 로 destination 에 surface 보존되지만, future caller 가 kebab-parse-types::* direct import 가 익숙해진 상태라면 ergonomic 회귀.
Mitigation:
kebab-parse-md::*의 single-crate import 경로가 newcomer 에게 더 직관적 (markdown parsing 의 unified surface).- 4 frozen task spec (p1-2, p1-3, p1-4, p9-fb-07) 이 explicit type-by-type 인용 (
use kebab_parse_types::ParsedBlock) → 이들은 frozen 으로 historical contract, mechanical update 없음. 새 caller (있다면) 는kebab-parse-md::ParsedBlock사용.
§6.6 R6 — DB schema 영향 (provenance_json BLOB)
위험: provenance_json BLOB 안의 agent string 값이 변경되면 (Q1 갱신 정책 선택 시) old DB 의 entry 와 new DB 의 entry 가 diverge — UI 가 그 차이를 surface 하지 않으나, future analytic query 가 stale 한 WHERE agent = 'kb-normalize' filter 를 적용하면 row miss.
Mitigation:
- §3.7e 의 보존 정책 (권장) 이 본 위험 0.
- 만약 critic round 에서 Q1 을 "갱신" 으로 결정 시, V00X migration 또는 lazy re-classification helper 추가 — 본 spec 의 scope 외 (별 PR).
§6.7 R7 — 16 GB RAM 의 build pressure
위험: CLAUDE.md 의 "Serial cargo builds only" 룰 (MEMORY.md). 본 흡수 PR 의 verification 이 cargo test --workspace --no-fail-fast -j 1 1회 + cargo clippy --workspace --all-targets -- -D warnings 1회 = 2회 full build. lance/datafusion link step 의 RAM pressure 가 PR-9c-2 머지 시점에 확인된 적 있음.
Mitigation:
- per-crate 단위 검증 (
cargo test -p kebab-parse-md+cargo test -p kebab-app) 을 plan 단계에서 우선 → full workspace 는 verifier round 1회. cargo clean직전 후 (CLAUDE.md 룰 — "Runcargo cleanroutinely after each merged PR").
§6.8 R8 — tracing instrumentation 의 회귀
위험: kebab-normalize 의 tracing::* calls 가 destination 으로 이식 후 module path 변경 (kebab_normalize::lib::... → kebab_parse_md::normalize::...). log scraper (있다면) 의 module-path filter 가 stale.
Mitigation:
~/.local/state/kebab/logs/kb.log.YYYY-MM-DD의 grep pattern (있다면) 갱신 안내 — README / SMOKE.md 변경 없음 (verified, internal 항목).- log scraper 자체가 user-visible surface 아님 (developer-facing) → wire / surface impact 0 유지.
§6.9 R9 — kebab-parse-md 의 dependency 폭증
위험: 흡수 후 kebab-parse-md/Cargo.toml 의 deps 가 기존 (kebab-core, pulldown-cmark, serde_yaml_ng, toml, lingua, tracing …) + 흡수 (unicode-normalization) 로 폭증. lingua 의 build time + binary size 가 markdown parse + lift 두 책임을 모두 가지는 crate 에 concentrate.
Mitigation:
- 신규 deps 추가 =
unicode-normalization1개만 (이미kebab-app도 사용 중인0.1major). version drift 없음. - 다른 deps 는 모두 흡수 전
kebab-parse-md에 이미 존재. - 본 위험의 실질 영향 ≈ +1 dep (
unicode-normalization) → 영향 minimal.
§6.10 R10 — frozen p1-4 surface (pub use kebab_core::{id_for_block, id_for_doc}) re-export 제거
위험: tasks/p1/p1-4-normalize.md:60-62 의 frozen public surface 가 kebab-normalize::{id_for_block, id_for_doc} re-export 를 명시. 본 spec §3.3 의 결정 (re-export 제거) 가 그 frozen surface 의 후퇴.
Mitigation (production caller 0 검증):
# kebab-normalize::id_for_* re-export 의 production caller 검색 (목표 = 0 hit).
$ grep -rn "kebab_normalize::id_for_\|use kebab_normalize::{.*id_for" crates/*/src/
(0 hit)
# 비교: kebab_core::id_for_* 직접 import 의 caller (production + test mod 모두):
$ grep -rn "id_for_block\|id_for_doc" crates/*/src/
crates/kebab-chunk/src/code_*_ast_v1.rs:187 ← #[cfg(test)] mod tests 안의 import (production 아님)
crates/kebab-chunk/src/code_*_ast_v1.rs:196 ← #[cfg(test)] mod tests 안의 call
crates/kebab-chunk/src/code_*_ast_v1.rs:207 ← #[cfg(test)] mod tests 안의 call
crates/kebab-parse-md/src/frontmatter.rs:592 ← #[cfg(test)] mod tests 안의 import (production 아님)
crates/kebab-parse-md/src/frontmatter.rs:737-738 ← #[cfg(test)] mod tests 안의 call
crates/kebab-normalize/src/lib.rs ← production (`build_canonical_document` body 의 `id_for_doc(&asset.workspace_path, &asset.asset_id, parser_version)` direct call, lib.rs:66 부근)
→ production code 의 id_for_* direct caller = kebab-normalize::lib.rs 자신만 (lift body 안의 single call). 다른 모든 id_for_* hit 은 #[cfg(test)] mod tests 안의 fixture builder. kebab-normalize::id_for_* re-export 경유 production caller = 0 verified.
→ frozen surface 의 후퇴이나 production caller 0 → real-world 영향 0. tasks/p1/p1-4 의 frozen 룰은 historical contract 로 보존 — 본 PR 의 HOTFIXES entry (§3.9) 가 live source. 본 R10 은 critic round 에서 Q3 (lib.rs re-export glob vs explicit) 와 함께 closure 요망.
§7 Wire / surface impact
| surface | impact | 근거 |
|---|---|---|
wire schema (docs/wire-schema/v1/*.json 16 file) |
0 | §1.9 의 11 schema 0 hit + §5.4 의 git diff = 0 |
--json 출력 (ingest_report, search_hit, answer, chunk_inspection, doc_summary, error, …) |
0 | provenance 가 어떤 schema 에도 export 되지 않음 |
CLI (kebab 의 subcommand + flag + exit code) |
0 | facade 변경 0, CLI 의 surface 는 kebab-app::*_with_config 의 signature 보존 |
TUI (kebab tui 의 키 binding + pane) |
0 | UI crate 영향 0 |
MCP (kebab-mcp 의 tool definitions + JSON-RPC) |
0 | MCP 가 kebab-app 통해 호출 — facade signature 보존 |
config (config.toml field, KEBAB_* env, XDG path) |
0 | 변경 0 |
Cargo.toml workspace.version |
0.18.0 → 0.19.0 | frozen design contract (§3.7b) 변경 trigger — CLAUDE.md "Release / binary version bump" |
Cargo.lock |
auto-갱신 | cargo build 후 자동 |
Cargo.toml workspace.members count |
24 → 22 | §3.8 |
| README.md | 0 | user-facing 변경 0 |
| HANDOFF.md | 1 줄 추가 | "머지 후 발견된 버그 / 결정" 의 cross-link (HOTFIXES 2026-05-26) |
docs/ARCHITECTURE.md |
갱신 | crate graph + directory tree mechanical |
tasks/INDEX.md |
2 갱신 | (a) L169 의 defer mention closure, (b) "Future work / deferred" 섹션 신설 (없으면) + image/pdf normalize integration 한 줄 entry — §11 cross-link |
tasks/HOTFIXES.md |
신규 entry | §3.9 의 4-block 변형 (Action 라인에 §11 future surface cross-link 포함) |
| 4 frozen task spec (p1-2, p1-3, p1-4, p9-fb-07) | 0 | frozen 룰 |
| ~25 referencing task spec | 0 | frozen 룰 |
parser_version cascade |
0 | lift 로직 의미 보존 |
chunker_version, embedding_version, prompt_template_version, index_version |
0 | 영향 없음 |
| Cargo features | 0 | 변경 0 |
| SQLite schema (V00X migration) | 0 | documents.provenance_json 의 string 값 보존 정책 (§3.7e) |
--json error.v1 의 code field |
0 | 영향 없음 |
~/.local/state/kebab/logs/kb.log.YYYY-MM-DD 의 tracing::span module path |
mechanical 변경 | log scraper 가 user-visible surface 아님, README 변경 0 |
integration package (integrations/claude-code/kebab/SKILL.md) |
0 | wire schema 변경 0 → SKILL.md 갱신 trigger 없음 |
| binary release tag | v0.19.0 cut 필수 | CLAUDE.md "Bump 시점 = release 시점 같은 commit" + gitea-ops gitea-release v0.19.0 |
§8 Out of scope
본 spec 의 scope 외:
- Lens 1 (kebab-source-fs dep lightening): 이미 PR #185 머지 완료 (
d4395a3). 본 spec 과 독립. - Lens 3 (Extractor + Chunker dispatch unification): post-PR9 audit 의 sibling defer item — 별 spec + 별 PR.
- 4 parser 의 normalize 의존성 신규 추가: 현재 markdown 만 normalize 를 경유. pdf/image/code 의 normalize 경유는 future re-extraction trigger 의 발동 시점 (§3.5). image/pdf normalize integration 의 미구현 design intent 자체는 §11 (Future work) 에 영구 보존.
kebab-core의 도메인 타입 변경: 변경 없음.Block,CanonicalDocument,Provenance모두 그대로.- mechanical referencing task spec update: frozen 룰. live source = HOTFIXES.md.
- V00X migration (DB schema 변경): 발생 안 함 (
provenance_jsonBLOB 보존, string 값 정책 §3.7e). - wire schema v1 → v2 major bump: 발생 안 함 (§7 verified).
derive_title또는build_canonical_document의 signature 변경: 변경 없음. callsite 이동만.- 새 forward-declared struct 추가: 보존된 3 struct 외 추가 없음.
- post-absorb
kebab-parse-md의 internal refactor: scope 외 — 흡수가 1:1 이식이므로 destination 의 module 구조 재정렬 등은 follow-up PR.
§9 References
- design contract:
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md§3.7b (line 703-764) + §8 (line 1457-1491). - sub-item 1 (sibling defer):
docs/superpowers/specs/2026-05-26-source-fs-dep-lightening-spec.md(PR #185 완료). - audit log root:
tasks/INDEX.mdL169 (PR #181 의 system-architect review 결론). - frozen task spec —
tasks/p1/p1-2-parser-types.md,tasks/p1/p1-3-markdown-parser.md,tasks/p1/p1-4-normalize.md,tasks/p9/p9-fb-07-md-title-fallback.md. - CLAUDE.md (project) — "Spec contract" / "Task specs themselves stay frozen" / "Live deviations from the original contract go in
tasks/HOTFIXES.md" / "Release / binary version bump" / "Versioning cascade" / "Wire schema v1". - CLAUDE.md (machine) — "Serial cargo builds only" / "Disk layout: /build/ 우선".
- MEMORY.md — "Phase priorities — P8 deferred, P9 first" (audio 미존재 근거).
- wire schema directory:
docs/wire-schema/v1/(16 file 검증 — §1.9). - ProvenanceEvent definition:
crates/kebab-core/src/metadata.rs:65-72(agent field internal-only). - Provenance persistence:
crates/kebab-store-sqlite/src/documents.rs:726(provenance_jsonBLOB 직렬화).
§10 Round 1+ closure table
| Round | Reviewer | Verdict | Issues | Closure |
|---|---|---|---|---|
| 1 | critic (round 1) | REQUEST_CHANGES — 3 CRITICAL + 8 MAJOR + 4 MINOR + 2 NIT | 본 round 2 revision 에서 all-closed 처리 — §10.1 의 finding-by-finding closure 참조 | 본 spec 의 round 2 revision (2026-05-26, planner) |
| 2 | critic (round 2) | REQUEST_CHANGES — 2 NEW MAJOR + 3 NEW MINOR + 1 NEW NIT (round 1 의 22 finding 중 18 fully closed + 1 partial + 1 stale-line-ref + 1 inverse-closure + 1 already-complete + 1 false-positive 재확정) | 본 round 3 revision 에서 closed — §10.2 의 finding-by-finding closure 참조 | 본 spec 의 round 3 revision (2026-05-26, planner) |
§10.1 round 1 finding-by-finding closure
| ID | Severity | Finding | Closure |
|---|---|---|---|
| #1 | CRITICAL | §1.5 signature byte-mismatch (asset: &AssetInfo 등) |
CLOSED — §1.5 의 signature 정정 (actual source crates/kebab-normalize/src/lib.rs:60-66, :360 와 byte-identical). derive_title 의 &[Block] (lifted, NOT ParsedBlock) plan-time type-error 회피 inline 명시. tasks/p1/p1-4-normalize.md:54-62 cross-link 추가. |
| #2 | CRITICAL | §1.4 dev-dep claim 부정확 (kebab-parse-md 의 dev-dep 에 normalize / parse-types 없음) | CLOSED — §1.2 caller table 갱신 (reverse dev-dep: normalize → parse-md, store-sqlite → normalize, chunk → normalize). §1.4 의 잘못된 dev-dep row 텍스트 정정. §3.7 (f) 의 cargo behavior 명시. |
| #3 | CRITICAL | §11 미존재 (critic 오해) | FALSE-POSITIVE — §11 main header line 997 부터 §11.8 까지 실존 (grep -n "^## §11" spec verified post-round-3-end; round 1 시점에는 line 793 부터 시작했고 round 2 의 +136 line + round 3 의 +68 line revision 으로 shift). critic round 1 의 stale spec read 또는 section search 부정확 의심. action 불필요. Line reference 정책: §10.1 의 모든 line number 는 grep cross-check 후 명시 (NEW MINOR #N4 closure). |
| #4 | MAJOR | §3.7 (b) 의 byte-identical diff (의미 없음) | CLOSED — 첫 hunk 삭제 + crates/kebab-app/src/lib.rs:1119 line number 명시. |
| #5 | MAJOR | §3.6 의 새 forbidden bullet 중복 | CLOSED — kebab-parse-md → store / llm / embed ✗ 삭제 + commentary 한 줄로 대체 ("기존 parse-* → store/llm/embed ✗ 룰이 흡수된 lift 까지 자동 포함"). |
| #6 | MAJOR | §3.5 future trigger 1 의 audio P8 ambiguity | CLOSED — trigger 1 의 audio 항목에 "P8 도입 시 (현재 deferred, tasks/INDEX.md Phase 8 row 참조)" timing 명시. build_canonical_document input variant 변경 의 measurement trigger 명시. §11.6 와 cross-link. |
| #7 | MAJOR | §1.9 의 11 schema explicit (16 누락) | CLOSED — 16 row 모두 explicit expand. §4.1 wording "11 schema" → "16 wire schema". search_response.schema.json:35 의 description prose false-positive 명시. |
| #8 | MAJOR | §3.9 HOTFIXES 5-block (4-block 위반) | CLOSED — 5번째 block 의 내용을 Amends block 의 마지막 문장 (괄호 inline) 으로 흡수. 4-block 회복. |
| #9 | MAJOR | §3.3 의 id_for_* 후퇴 명시 부족 | CLOSED — table 의 row note 갱신 (crates/kebab-chunk/src/code_*_ast_v1.rs 7+ 곳 + kebab-parse-md/src/frontmatter.rs:592 모두 kebab_core 직접 import — production caller 0 검증). §6.10 R10 추가 (grep mitigation cmd). |
| #10 | MAJOR | §5.2 의 22 crate 검증 명령 fragile | CLOSED — cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length' 으로 robust 화 (또는 ls -d crates/*/ | wc -l). |
| #11 | MAJOR | §3.4 chunk + store-sqlite dev-dep migration 누락 | CLOSED — 두 crate 의 dev-dep diff explicit + 통합 test source 의 use kebab_normalize::*; → use kebab_parse_md::*; migration 명시. |
| #12 | MINOR | §1.2 의 evidence cmd 인용 | CLOSED — §1.2 의 caller table heading 에 cat crates/.../Cargo.toml | grep -A20 "dev-dependencies" 명시. |
| #13 | MINOR | §3.5 의 parenthesis wording | CLOSED — **보존된 surface (계속)** block + tracing instrumentation block 으로 통합. parenthesis 풀어 body 문장. |
| #14 | MINOR | §1.6 의 P8 deferred cross-link generic-phrasing | CLOSED — MEMORY.md reference 를 tasks/INDEX.md 의 Phase 8 row 참조로 generic 화. |
| #15-16 | NIT | §3.7 (e) SQL 예제 + §6.7 RAM mitigation — 둘 다 정확 | NO-ACTION (정확 — critic 가 명시). |
| (Missing) #1 | Missing | §11 신설 | ALREADY-COMPLETE — §11 line 793-862 실존 (round 1 revision 시 완료). |
| (Missing) #2 | Missing | chunk + store-sqlite test source use migration |
CLOSED — §3.4 의 chunk + store-sqlite dev-dep diff 와 함께 명시. |
| (Missing) #3 | Missing | cargo deny workspace dep validation |
CLOSED — §5.9 신설. |
| (Missing) #4 | Missing | target/ clean policy |
CLOSED — §5.10 신설 (CLAUDE.md 룰 cross-link). |
| (Missing) #5 | Missing | Cargo.lock 변경 검증 |
CLOSED — §5.11 신설 (kebab-normalize / kebab-parse-types [[package]] 0 hit + unicode-normalization 추가 검증). |
| (Missing) #6 | Missing | tracing instrumentation target string 정책 |
CLOSED — §3.5 의 "Tracing instrumentation policy" block 신설. |
| (Ambiguity) #1 | Ambiguity | §3.7 (f) 자기 참조 dev-dep cargo standard behavior | CLOSED — §3.7 (f) 의 wording 갱신 (cargo standard behavior 명시 + cargo test -p kebab-parse-md --test normalize_snapshot verification cmd). |
| (Ambiguity) #2 | Ambiguity | §1.9 의 wire 정의 | CLOSED — §1.9 의 "wire 의 정의 (본 spec 범위 내)" block 신설 (JSON-RPC + CLI --json + 외부 통합. SQLite BLOB 는 wire 외). |
§10.2 round 2 finding-by-finding closure
| ID | Severity | Finding | Closure |
|---|---|---|---|
| #N1 | NEW MAJOR | §3.5 "Tracing instrumentation policy" 가 actual code 와 정반대 (자동 derive 가정 부정확 — actual lib.rs:109 의 target: "kebab-normalize" literal hard-coded) |
CLOSED — §3.5 wording 정정 (explicit literal 명시 + manual 갱신 필요 명시). §3.7 (g) 신설 — tracing::debug! target literal 의 보존 정책 (stage label 일관성, log scraper grep 호환). §6.8 R8 mitigation 의 1-line touch site 명시 (실제로 §3.5 + §3.7 (g) 가 mitigation 본체). |
| #N2 | NEW MAJOR | §3.7e + §1.9 의 warning_agent return string 정책 wording 부정확 (warning_agent 자체는 "kb-parse-md" 단일 return; 별도 hard-coded "kb-normalize" literal 2 곳) | CLOSED — §3.7e 의 comment block 갱신 (warning_agent body = "kb-parse-md" 단일 + lib.rs:122/128/134/153 의 4 hard-coded literal 위치별 보존 정책 inline 명시). §1.9 의 production flow trace 를 6-row table (line / string / emitter / persist) 로 분리. |
| #N3 | NEW MINOR | §3.3 + §6.10 R10 의 evidence test-mod imports → production 으로 misclassify | CLOSED — §3.3 row note 정정 (production caller wording 일반화). §6.10 R10 의 grep cmd refresh — test mod import 위치는 #[cfg(test)] mod tests 안의 ... 명시. 진짜 production caller (kebab-normalize::lib.rs 자기 자신, lift body 의 single direct call) 명시. R10 결론 (production caller 0) 무변. |
| #N4 | NEW MINOR | §10.1 line reference stale (round 2 revision 의 +136 line shift 미반영) | CLOSED — §10.1 의 row #3 (CRITICAL #3 closure) 의 line reference 갱신 (793→978 + post-revision 의 shift 명시). 추가로 "Line reference 정책" inline 명시 — 향후 round 의 모든 line number 는 grep cross-check 후 명시. |
| #N5 | NEW MINOR | §6 의 R10 / R9 ordering 비정합 (R10 line 803 < R9 line 829) | CLOSED — 두 section 의 physical position 을 swap. 결과: §6.9 R9 (dep 폭증) → §6.10 R10 (frozen p1-4 surface) 의 natural ordering. label number = position number = risk introduction order. |
| #N6 | NEW NIT | §3.8 의 diff hunk 두 block 한 hunk 표현 (line-context mismatch 위험) | CLOSED — §3.8 의 diff 를 Hunk (a) [workspace] members 의 2 entry 삭제 + Hunk (b) [workspace.package] version 1-line 변경 의 두 hunk 로 분리. plan/executor 가 둘을 sequential 또는 parallel 로 적용 가능. |
| (round 1 의 18 fully closed + already-complete + false-positive 재확정) | — | round 1 의 §10.1 closure 가 critic round 2 의 evidence 와 정합 — actual byte-identical verify 통과 | NO-ADDITIONAL-ACTION. |
§10.3 round 3 metrics
- Spec line count: 863 (round 2 start) → 999 (round 2 end) → 1067 (round 3 end, post-N1~N6 + §10.1 line reference refresh + §10.3 placeholder fill).
- Section headers: 60 (round 2 start) → 65 (round 2 end) → 67 (round 3 end, §3.7 (g) tracing target + §10.2 round 2 closure table).
- §6 의 R9 ↔ R10 physical swap (label rename + content swap) — 신설 없음, ordering 정합 only.
- §3 Design 결정 무변 (Option A, dead struct 3 보존, §3.7b 4-단락 재작성, target_version 0.19.0, warning_agent + tracing target 보존 정책).
§11 Future work — image/pdf normalize integration (design §3.7b intent 의 미구현)
본 PR 은 kebab-parse-types 와 kebab-normalize 를 kebab-parse-md 로 흡수하지만, design §3.7b 의 원래 intent — "4 parser (md/pdf/image/audio) 가 각자 ParsedBlock 변종 emit → normalize 가 medium-agnostic 통합 lift" — 는 미구현 상태로 영구 보존된다. 본 섹션이 그 영구 보존 entry.
§11.1 배경
design §3.7b 의 원래 의도:
- 4 parser (md/pdf/image/audio) 가 각자 ParsedBlock 변종 (
ParsedBlock/ParsedImageRegion/ParsedPdfPage/ParsedAudioSegment) emit. kebab-normalize가 medium-agnostic ID/Provenance lift 수행.- 결과 = 모든 parser 가 동일
CanonicalDocumentshape 으로 합류. - 즉 normalize 는 multi-parser 통합 layer.
§11.2 현재 (v0.18.x) 상태
- markdown 만 의도된 path 사용 —
ParsedBlock→ normalize →CanonicalDocument. - image / pdf / code parser 는 normalize 우회, 직접
Extractor::extract() → CanonicalDocumentemit. - 3 forward-declared struct (
ParsedImageRegion/ParsedPdfPage/ParsedAudioSegment) caller = 0. - audio parser 자체가 미존재 (P8 deferred —
MEMORY.md"Phase priorities — P8 deferred, P9 first").
§11.3 본 PR (v0.19.0) 결정
kebab-normalize+kebab-parse-types흡수 →kebab-parse-md.- dead struct 3 보존 (design intent 자체는 유효 — future surface).
- design §3.7b strike → §3.5 의 4-단락 재작성 ("원래 intent + 현재 상태 + 보존된 surface + future re-extraction trigger" — raison d'être 약화이지 폐기 아님).
§11.4 Future direction (v0.20+ 후보)
다음 세 갈래가 image/pdf normalize integration 의 구체 시나리오. 본 PR 머지 후 followup spec / PR 의 trigger 후보:
- image parser normalize integration —
ImageExtractor::extract가Vec<ParsedImageRegion>emit →kebab-parse-md(흡수된 destination) 의build_canonical_document_from_image_regions(...)형태 lift fn 추가. multi-region image 활용 — text region (OCR) + caption region (LLM-generated description) + image region (raw bytes pointer) 의 region-별 provenance + chunk granularity 향상. - pdf parser normalize integration —
PdfTextExtractor::extract가Vec<ParsedPdfPage>emit → page-별 metadata + block 통합 lift. multi-block pdf 활용 — per-page provenance (page-N 단위 citation), cross-page reference (forward-ref / back-ref 감지), 또는 page-별 doc-summary. - audio parser introduction (P8 재개 또는 P+ phase) —
ParsedAudioSegment의 첫 production caller. segment-level timestamp + speaker provenance. 현재 P8 audio 사용자 결정 deferred (MEMORY.md), 재개 시 본 §11.4.3 가 entry point.
§11.5 본 PR 의 future-proofing
- 3 dead struct 보존 → 미래 도입 시 type 재정의 cost 0.
pubre-export 유지 (§3.3) 로 caller add 시점에 surface 변경 0. - design §3.7b strike wording (§3.5) 이 "abstraction dead by P+ usage gap" 으로 한정 — raison d'être 자체는 보존, 4-단락 구조 (원래 intent 보존 + future trigger 명시).
- 본 PR 의 흡수로
kebab-parse-md가 multi-parser lift 의 단일 destination 가 됨 — future direction 도입 시 추가 caller (image / pdf / audio) 가kebab-parse-md의 lift fn 을 호출하는 패턴으로 자연 합류 (§3.7b 의 fan-in 회복). warning_agent의 stage label "kb-normalize" 보존 (§3.7e) 로 future caller 가 자기 stage label 추가 시 ("kb-parse-image-normalize"등) 의미 충돌 없이 확장 가능.
§11.6 Trigger 조건
다음 중 하나 발생 시 v0.20+ scope 진입 (별 spec + 별 PR):
- image parser 에서 multi-region 분리 요구 — search granularity 향상 (region-별 chunk) 또는 LLM caption-only vs caption+text 비교 시.
- pdf parser 에서 page-level metadata 통합 필요 — cross-page reference 감지, 또는 page-별 doc-summary surface.
- audio parser 도입 — whisper.cpp local transcription 활성화 (P8 재개 trigger).
- fan-in ≥ 2 회복 — 위 1~3 중 2개 이상 동시 도입 시 §3.7b 의 layer 가치 회복 →
kebab-parse-md에서kebab-normalizere-extract 검토.
§11.7 본 PR 의 deliverable (§11 관련)
- spec §11 자체 (본 섹션) — 영구 보존 entry.
tasks/INDEX.md의 "Future work / deferred" 섹션 (없으면 신설) 에 한 줄 entry:## Future work / deferred - v0.20+ image/pdf normalize integration — design §3.7b intent 미구현 (3 dead struct 보존). PR #186 (normalize-absorption) 의 spec §11 참조.tasks/HOTFIXES.md의 2026-05-26 entry Action 라인에 §11 cross-link (§3.9 에 반영).
§11.8 §11 이 critic 검토에서 손대지 말아야 할 결정
본 PR 의 §3 Design 결정 (Option A destination, dead struct 3 보존, §3.7b 4-단락 재작성, warning_agent 보존 정책) 는 §11 도입과 정합 — critic 검토에서 §11 추가가 §3 결정을 흔들면 안 됨. 만약 흔드는 결과 도출 시 §11 자체의 재배치 (별 spec 으로 split, 또는 §6 Risk 의 R4 로 흡수) 를 권장하고 §3 는 유지.
Spec drafted by: planner (team normalize-absorption, Phase A).
Date: 2026-05-26.
Status: drafting → critic round 대기.
Revision: 본 spec 의 §11 추가 (image/pdf normalize integration 의 future-work 영구 보존) — team-lead 추가 요청 반영 (2026-05-26).