Files
kebab/docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md
altair823 710945c4b0 refactor(parse-md): absorb kebab-normalize + kebab-parse-types — 24 → 22 crates + §3.7b 재작성
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 줄.
2026-05-26 15:00:59 +00:00

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 갱신)
§3.7b
§8
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md
docs/superpowers/specs/2026-05-26-source-fs-dep-lightening-spec.md

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.rsCargo.toml [dev-dependencies] kebab-parse-md 로 reverse-direction dev-dep — kebab-parse-md 를 fixture parser 로 사용 reverse dev-dep

→ production caller = kebab-app 단일. kebab-normalize 흡수 시:

  1. kebab-app 의 1 줄 use statement + call site (lib.rs:51, :1119) 갱신.
  2. kebab-chunk + kebab-store-sqlite 의 dev-dep kebab-normalizekebab-parse-md 로 갈음 + 두 crate 의 통합 test source (tests/*.rs) 의 use kebab_normalize::*;use kebab_parse_md::*; 갱신.
  3. kebab-normalize/tests/normalize_snapshot.rskebab-parse-md reverse 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, :360tasks/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 — 내부에서 user map 의 title / langremove 하면서 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 점으로 정당화:

  1. namespace 폭발 방지: parser-별 ParsedBlock 변종이 kebab-core 의 namespace 를 폭발시키지 않도록 thin layer 분리.
  2. 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::Block namespace 폭발 우려도 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:35hint 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 필드로 누적.
  • Provenanceserde_json::to_string(&doc.provenance) 로 직렬화되어 documents.provenance_json BLOB 컬럼에 persist (crates/kebab-store-sqlite/src/documents.rs:726).
  • wire export 경로 0: 어떤 --json 출력에도 provenance field 가 노출되지 않음. 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 의 dead kebab-parse-types regular 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_document body 와 derive_title body 가 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_version cascade 변경 (lift 로직 보존이므로 cascade trigger 없음).
  • p9-fb-07 의 derive_title API 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 variant ParsedPayload enum + 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-mdlib.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:51use 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:109explicit 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-typeskebab-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-mdpub 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:

  1. warning_agent 의 body: 4 WarningKind variant 모두 "kb-parse-md" 단일 return — warning_agent 자체는 "kb-normalize" string 을 반환 안 함 (crates/kebab-normalize/src/lib.rs:187-191).

  2. 별도 hard-coded "kb-normalize" literal 2 곳 (warning_agent 와 별개):

    • lib.rs:134Normalized event 의 agent field (build_canonical_document body 의 lift 종료 시점 ProvenanceEvent push).
    • lib.rs:153lift_warnings loop 의 agent field (lift-stage warning 의 attribution — 현재 AudioRef-deferred drops only).
  3. 또 다른 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.tomlkebab-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:15kebab-parse-types = { path = "../kebab-parse-types" } regular dep declare.
  • grep -rn "kebab_parse_types" crates/kebab-app/src/: 0 hit.
  • kebab-appkebab-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 에서 결정)

  • Q1warning_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 에서 재확정.
  • Q2kebab-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 의견.
  • Q3kebab-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 의견.
  • Q4kebab-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/*.rsuse kebab_parse_md::*; 로 직접 link). critic round verification 으로 cargo 동작 확인 필요.
  • Q5 — kebab-chunk + kebab-store-sqlite 의 kebab-normalize dev-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/*.json 16 파일 모두 변경 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.md L169 의 "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.rskebab-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/*.rsuse 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 룰 — "Run cargo clean routinely after each merged PR").

§6.8 R8 — tracing instrumentation 의 회귀

위험: kebab-normalizetracing::* 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-normalization 1개만 (이미 kebab-app 도 사용 중인 0.1 major). 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 codeid_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.v1code field 0 영향 없음
~/.local/state/kebab/logs/kb.log.YYYY-MM-DDtracing::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_json BLOB 보존, 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.md L169 (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_json BLOB 직렬화).

§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 중복 CLOSEDkebab-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 CLOSEDcargo 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 CLOSEDMEMORY.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:109target: "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-typeskebab-normalizekebab-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 가 동일 CanonicalDocument shape 으로 합류.
  • 즉 normalize 는 multi-parser 통합 layer.

§11.2 현재 (v0.18.x) 상태

  • markdown 만 의도된 path 사용 — ParsedBlock → normalize → CanonicalDocument.
  • image / pdf / code parser 는 normalize 우회, 직접 Extractor::extract() → CanonicalDocument emit.
  • 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 후보:

  1. image parser normalize integrationImageExtractor::extractVec<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 향상.
  2. pdf parser normalize integrationPdfTextExtractor::extractVec<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.
  3. 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. pub re-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):

  1. image parser 에서 multi-region 분리 요구 — search granularity 향상 (region-별 chunk) 또는 LLM caption-only vs caption+text 비교 시.
  2. pdf parser 에서 page-level metadata 통합 필요 — cross-page reference 감지, 또는 page-별 doc-summary surface.
  3. audio parser 도입 — whisper.cpp local transcription 활성화 (P8 재개 trigger).
  4. fan-in ≥ 2 회복 — 위 1~3 중 2개 이상 동시 도입 시 §3.7b 의 layer 가치 회복 → kebab-parse-md 에서 kebab-normalize re-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).