diff --git a/Cargo.lock b/Cargo.lock index 13becb3..f64dda4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4127,7 +4127,7 @@ dependencies = [ [[package]] name = "kebab-app" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -4143,12 +4143,10 @@ dependencies = [ "kebab-llm", "kebab-llm-local", "kebab-nli", - "kebab-normalize", "kebab-parse-code", "kebab-parse-image", "kebab-parse-md", "kebab-parse-pdf", - "kebab-parse-types", "kebab-rag", "kebab-search", "kebab-source-fs", @@ -4173,12 +4171,11 @@ dependencies = [ [[package]] name = "kebab-chunk" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", "kebab-core", - "kebab-normalize", "kebab-parse-code", "kebab-parse-md", "serde_json", @@ -4190,7 +4187,7 @@ dependencies = [ [[package]] name = "kebab-cli" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "clap", @@ -4211,7 +4208,7 @@ dependencies = [ [[package]] name = "kebab-config" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "dirs 5.0.1", @@ -4226,7 +4223,7 @@ dependencies = [ [[package]] name = "kebab-core" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4240,7 +4237,7 @@ dependencies = [ [[package]] name = "kebab-embed" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4254,7 +4251,7 @@ dependencies = [ [[package]] name = "kebab-embed-local" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "fastembed", @@ -4267,7 +4264,7 @@ dependencies = [ [[package]] name = "kebab-eval" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "kebab-app", @@ -4286,7 +4283,7 @@ dependencies = [ [[package]] name = "kebab-llm" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "kebab-core", @@ -4295,7 +4292,7 @@ dependencies = [ [[package]] name = "kebab-llm-local" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "kebab-config", @@ -4312,7 +4309,7 @@ dependencies = [ [[package]] name = "kebab-mcp" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "kebab-app", @@ -4330,7 +4327,7 @@ dependencies = [ [[package]] name = "kebab-nli" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "hf-hub", @@ -4343,24 +4340,9 @@ dependencies = [ "tracing", ] -[[package]] -name = "kebab-normalize" -version = "0.18.0" -dependencies = [ - "anyhow", - "kebab-core", - "kebab-parse-md", - "kebab-parse-types", - "serde", - "serde_json", - "time", - "tracing", - "unicode-normalization", -] - [[package]] name = "kebab-parse-code" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "gix", @@ -4383,7 +4365,7 @@ dependencies = [ [[package]] name = "kebab-parse-image" -version = "0.18.0" +version = "0.19.0" dependencies = [ "ab_glyph", "anyhow", @@ -4407,11 +4389,10 @@ dependencies = [ [[package]] name = "kebab-parse-md" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "kebab-core", - "kebab-parse-types", "lingua", "pulldown-cmark", "serde", @@ -4420,11 +4401,12 @@ dependencies = [ "time", "toml", "tracing", + "unicode-normalization", ] [[package]] name = "kebab-parse-pdf" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4435,17 +4417,9 @@ dependencies = [ "tracing", ] -[[package]] -name = "kebab-parse-types" -version = "0.18.0" -dependencies = [ - "kebab-core", - "serde", -] - [[package]] name = "kebab-rag" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4467,7 +4441,7 @@ dependencies = [ [[package]] name = "kebab-search" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "globset", @@ -4486,7 +4460,7 @@ dependencies = [ [[package]] name = "kebab-source-fs" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4504,7 +4478,7 @@ dependencies = [ [[package]] name = "kebab-store-sqlite" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "blake3", @@ -4512,7 +4486,6 @@ dependencies = [ "kebab-chunk", "kebab-config", "kebab-core", - "kebab-normalize", "kebab-parse-md", "refinery", "rusqlite", @@ -4525,7 +4498,7 @@ dependencies = [ [[package]] name = "kebab-store-vector" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "arrow", @@ -4549,7 +4522,7 @@ dependencies = [ [[package]] name = "kebab-tui" -version = "0.18.0" +version = "0.19.0" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index e1e7c83..27f589d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,9 @@ 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", "crates/kebab-store-sqlite", "crates/kebab-store-vector", @@ -32,7 +30,7 @@ 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" # pre-v0.18 workspace-wide cleanup: enable clippy::pedantic group with # intentional allow-list. The allowed lints are either cosmetic (doc style), diff --git a/HANDOFF.md b/HANDOFF.md index 0e01339..ab8f6f5 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -32,6 +32,7 @@ P0~P5 직렬. P6~P9 P5 이후 병렬 가능. 머지 후 발견된 모든 deviation / hotfix 의 dated 로그는 [tasks/HOTFIXES.md](tasks/HOTFIXES.md). 본 요약은 \"누군가가 인수받을 때 알아두면 시간을 많이 절약하는\" 항목만: +- **2026-05-26 kebab-normalize + kebab-parse-types 흡수 (24 → 22 crates, design §3.7b 재작성)** — v0.19.0 cut. 4 parser 중 markdown 한 갈래만 lift 를 경유하는 reality 가 design §3.7b 의 fan-in ≥ 2 가정과 diverge → thin layer (`kebab-parse-types`) + `kebab-normalize` 두 crate 가 `kebab-parse-md` 로 흡수. 5 사용 type + 3 forward-declared struct 모두 `kebab-parse-md::{types,normalize}` module 의 `pub` re-export 로 보존. wire / surface impact = 0 (CLI / TUI / MCP / `--json` / config / XDG / parser_version 모두 unchanged). 자세한 내용: `tasks/HOTFIXES.md` (2026-05-26 design deviation entry). - **2026-05-26 v0.18.0 fb-41 multi-hop RAG + NLI verification ship (PR #176-180) + post-PR9 cleanup (PR #181)** — pre-v0.18.0 dogfood (`/build/cache/dogfood-v018/`, 33 assets / 205 chunks, gemma3:4b CPU only / 16 GB RAM) 에서 발견된 S7 caffeine hallucination 의 root cause = LLM-self-judge ceiling (synthesize 가 chunks 와 무관한 Adam optimizer gradient 식을 silent emit, self-judge 가 reject 못함). 학계 표준 (Self-RAG, CRAG, Auto-GDA, MedTrust-RAG) 결론 = deterministic post-synthesis verification. mDeBERTa-v3 XNLI ONNX (280 MB, Xenova HF) 가 `(packed_chunks, answer)` entailment 검사 — `[rag] nli_threshold > 0` (default 0.0 = disabled, production 권장 0.5) 일 때 활성. dogfood retest 측정 — S7 PR-8 baseline `grounded=true + Adam hallucination` → PR-9 `nli_verification_failed, nli_score 0.0035`. wire additive minor — `answer.v1.verification` field + `refusal_reason` 의 `nli_verification_failed` / `nli_model_unavailable` 추가, pre-v0.18 reader 무영향. 5 sub-PR 시퀀스 + cleanup PR (clippy::pedantic baseline + 의도적 30+ allow + H1 `[models.nli].model` config wiring + 9 new tests). post-refactor retest = PR-9d byte-identical (deterministic 확인). 자세한 내용: `tasks/HOTFIXES.md` (2026-05-25 fb-41 PR-9 closure entry + S3 follow-up). - **2026-05-25 v0.17.2 post-v0.17.1 polish (PR #164 + #165)** — v0.17.1 의 두 follow-up closure. (1) `[image.ocr] request_timeout_secs` 별 노브 — `crates/kebab-parse-image/src/ocr.rs::REQUEST_TIMEOUT` hard 300s 제거, LLM 쪽 패턴 (PR #162) 을 OCR 어댑터에 동일 적용. 사용자 결정으로 별 노브 분리 (OCR vs LLM 의 cold start 패턴이 달라 독립 조절). v0.17.1 미진행 항목 closure. (2) `chunks_fts` 의 `heading_path` 컬럼이 JSON 표기 + path 세그먼트 까지 trigram 색인 → query false positive 가능 문제 closure. `lexical.rs::build_match_string` 가 non-raw 분기 결과를 `text : ()` 로 wrap — heading 색인 V007 verbatim 유지, 매칭만 text 한정. 사용자가 명시 heading 검색 하려면 raw mode `'heading_path : '` escape hatch (SKILL.md 갱신). 둘 다 additive (옛 config 호환) / re-ingest 불필요. 자세한 내용: `tasks/HOTFIXES.md` (2026-05-25 v0.17.2 두 entry). - **2026-05-25 v0.17.1 post-dogfood (PR #162 + #163)** — 확장 도그푸딩 (16 GB CPU only, gemma4:e4b 시도) 에서 발견된 두 follow-up 한 묶음. (1) `crates/kebab-llm-local/src/ollama.rs::REQUEST_TIMEOUT` hard 300s → `[models.llm] request_timeout_secs` config + env override (additive, default 300, `=0` 은 disable 아닌 "즉시 timeout" 이라 doc 명시). (2) README + SMOKE 에 sudo / systemd 없이 ollama 설치 + ≤4B Q4 권장 모델 + `kebab ask --stream` UX 권장 docs. additive only — 옛 config / wire 호환. 자세한 내용: `tasks/HOTFIXES.md` (2026-05-25). diff --git a/crates/kebab-app/Cargo.toml b/crates/kebab-app/Cargo.toml index b506bba..f1b991e 100644 --- a/crates/kebab-app/Cargo.toml +++ b/crates/kebab-app/Cargo.toml @@ -12,8 +12,6 @@ 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-store-sqlite = { path = "../kebab-store-sqlite" } kebab-store-vector = { path = "../kebab-store-vector" } diff --git a/crates/kebab-app/src/lib.rs b/crates/kebab-app/src/lib.rs index 1eb8caf..2378e2e 100644 --- a/crates/kebab-app/src/lib.rs +++ b/crates/kebab-app/src/lib.rs @@ -48,11 +48,10 @@ use kebab_core::{ SourceUri, VectorRecord, VectorStore, }; use kebab_llm_local::OllamaLanguageModel; -use kebab_normalize::build_canonical_document; use kebab_parse_image::{ImageExtractor, OllamaVisionOcr, apply_caption, apply_ocr}; use kebab_parse_code::{CAstExtractor, CppAstExtractor, GoAstExtractor, JavaAstExtractor, JavascriptAstExtractor, KotlinAstExtractor, PythonAstExtractor, RustAstExtractor, TypescriptAstExtractor}; use kebab_parse_pdf::PdfTextExtractor; -use kebab_parse_md::{BodyHints, parse_blocks, parse_frontmatter}; +use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter}; use kebab_source_fs::FsSourceConnector; mod app; @@ -1116,7 +1115,7 @@ fn ingest_one_asset( parser_version, all_warnings, ) - .context("kb-normalize::build_canonical_document")?; + .context("kb-parse-md::build_canonical_document")?; let chunks = MdHeadingV1Chunker .chunk(&canonical, chunk_policy) diff --git a/crates/kebab-chunk/Cargo.toml b/crates/kebab-chunk/Cargo.toml index 67228ba..80ccea4 100644 --- a/crates/kebab-chunk/Cargo.toml +++ b/crates/kebab-chunk/Cargo.toml @@ -16,14 +16,14 @@ tracing = { workspace = true } serde_yaml = { workspace = true } [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. +# kb-parse-md / kb-parse-code are dev-only — used by the snapshot integration +# tests to build a CanonicalDocument from fixture files. kb-parse-md absorbed +# kb-normalize in v0.19.0 (HOTFIXES.md 2026-05-26). 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 } diff --git a/crates/kebab-chunk/tests/long_section_snapshot.rs b/crates/kebab-chunk/tests/long_section_snapshot.rs index ef2c982..e335a68 100644 --- a/crates/kebab-chunk/tests/long_section_snapshot.rs +++ b/crates/kebab-chunk/tests/long_section_snapshot.rs @@ -18,8 +18,7 @@ use kebab_core::{ AssetId, AssetStorage, Checksum, ChunkPolicy, ChunkerVersion, Chunker, MediaType, ParserVersion, RawAsset, SourceUri, WorkspacePath, }; -use kebab_normalize::build_canonical_document; -use kebab_parse_md::{BodyHints, parse_blocks, parse_frontmatter}; +use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter}; use serde_json::Value; use time::OffsetDateTime; diff --git a/crates/kebab-normalize/Cargo.toml b/crates/kebab-normalize/Cargo.toml deleted file mode 100644 index 493c7b0..0000000 --- a/crates/kebab-normalize/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "kebab-normalize" -version = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -license = { workspace = true } -repository = { workspace = true } -description = "Lift parser output (kb-parse-types) into kb-core::CanonicalDocument with deterministic IDs (§3.4, §4.2, §4.3)" - -[dependencies] -kebab-core = { path = "../kebab-core" } -kebab-parse-types = { path = "../kebab-parse-types" } -serde = { workspace = true } -serde_json = { workspace = true } -unicode-normalization = "0.1" -time = { workspace = true } -anyhow = { workspace = true } -tracing = { workspace = true } - -[dev-dependencies] -# kb-parse-md is permitted as a *dev*-dependency only — used by the -# integration snapshot test to drive a fixture through the real parser. -# Forbidden as a regular dep per design §8 (kb-normalize must not depend -# on any specific parser); `cargo tree -p kb-normalize --depth 1` (the -# default scope, excluding dev-deps) confirms this. -kebab-parse-md = { path = "../kebab-parse-md" } -serde_json = { workspace = true } - -[lints] -workspace = true diff --git a/crates/kebab-parse-md/Cargo.toml b/crates/kebab-parse-md/Cargo.toml index 7256b63..642ee44 100644 --- a/crates/kebab-parse-md/Cargo.toml +++ b/crates/kebab-parse-md/Cargo.toml @@ -5,16 +5,19 @@ edition = { workspace = true } rust-version = { workspace = true } license = { workspace = true } repository = { workspace = true } -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 } +# 흡수된 kb-normalize 의 NFKC 의존 — actual kebab-normalize/src/lib.rs:31 의 +# `use unicode_normalization::UnicodeNormalization;` 가 normalize.rs 이식 시 동반. +# 이미 kebab-app 도 사용 중인 0.1 major (version drift 0). +unicode-normalization = "0.1" # pulldown-cmark is the CommonMark parser used by the `blocks` submodule. # GFM tables are gated by the runtime `Options::ENABLE_TABLES` flag, not a # cargo feature; we strip the default `getopts` + `html` features since we diff --git a/crates/kebab-parse-md/src/blocks.rs b/crates/kebab-parse-md/src/blocks.rs index c3b277c..71d03d6 100644 --- a/crates/kebab-parse-md/src/blocks.rs +++ b/crates/kebab-parse-md/src/blocks.rs @@ -1,4 +1,4 @@ -//! Markdown body → flat `Vec` (§3.4 / §3.7b). +//! Markdown body → flat `Vec` (§3.4 / §3.7b). //! //! Uses `pulldown-cmark` (with GFM tables enabled at runtime via //! `Options::ENABLE_TABLES`) to walk the body once and emit a flat list of @@ -22,7 +22,7 @@ //! [`kebab_core::Inline`] only models `Text | Code | Link | Strong | Emph`. //! Inline images, footnotes, hard breaks, etc. are dropped silently per //! design §3.4. Block-level `![alt](src)` (an image as the sole content of a -//! paragraph) is lifted to [`kebab_parse_types::ParsedPayload::ImageRef`]. +//! paragraph) is lifted to [`crate::types::ParsedPayload::ImageRef`]. //! //! ## CRLF //! @@ -34,7 +34,7 @@ use std::ops::Range; use kebab_core::{Inline, SourceSpan}; -use kebab_parse_types::{ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind}; +use crate::types::{ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind}; use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd}; /// Parse a Markdown body into a flat `Vec` plus any warnings. @@ -1586,7 +1586,7 @@ mod tests { let (blocks, _) = parse(body, 1); assert_eq!(blocks.len(), 1, "expected single list block"); match &blocks[0].kind { - kebab_parse_types::ParsedBlockKind::List => {} + crate::types::ParsedBlockKind::List => {} other => panic!("expected list, got {other:?}"), } } diff --git a/crates/kebab-parse-md/src/frontmatter.rs b/crates/kebab-parse-md/src/frontmatter.rs index 6ff78bc..cd27a1c 100644 --- a/crates/kebab-parse-md/src/frontmatter.rs +++ b/crates/kebab-parse-md/src/frontmatter.rs @@ -19,7 +19,7 @@ use std::ops::Range; use std::sync::OnceLock; use kebab_core::{Metadata, SourceType, TrustLevel}; -use kebab_parse_types::{Warning, WarningKind}; +use crate::types::{Warning, WarningKind}; use lingua::{IsoCode639_1, Language, LanguageDetector, LanguageDetectorBuilder}; use serde::Deserialize; use serde_json::{Map, Value}; diff --git a/crates/kebab-parse-md/src/lib.rs b/crates/kebab-parse-md/src/lib.rs index 8b893b7..8e7c083 100644 --- a/crates/kebab-parse-md/src/lib.rs +++ b/crates/kebab-parse-md/src/lib.rs @@ -1,4 +1,8 @@ -//! `kb-parse-md` — Markdown parsing for the KB pipeline (§3.7b). +//! `kb-parse-md` — Markdown parsing + canonical-document lift for the KB pipeline (§3.7b). +//! +//! v0.19.0 부터 `types` + `normalize` module 은 in-crate 흡수 +//! (`kebab-parse-types` + `kebab-normalize` 의 historical crate 가 본 crate 로 +//! collapse — see HOTFIXES.md 2026-05-26). //! //! Public surface: //! @@ -11,15 +15,33 @@ //! * [`parse_blocks`] — pure function from Markdown body bytes to //! `(Vec, Vec)` with heading paths and 1-indexed //! `SourceSpan::Line` ranges relative to the original file (P1-3). +//! * [`build_canonical_document`] / [`derive_title`] — lift a parsed +//! markdown document into a `kebab_core::CanonicalDocument` (absorbed +//! from `kebab-normalize` — P1-4 / p9-fb-07 frozen API). +//! * Parser intermediate types ([`ParsedBlock`], [`ParsedBlockKind`], +//! [`ParsedPayload`], [`Warning`], [`WarningKind`]) and 3 forward-declared +//! structs ([`ParsedImageRegion`], [`ParsedPdfPage`], [`ParsedAudioSegment`]) — +//! absorbed from `kebab-parse-types`. //! //! Anything else in this crate is `pub(crate)` and may change without notice. pub mod blocks; pub mod frontmatter; +mod normalize; +mod types; pub use blocks::parse_blocks; pub use frontmatter::{BodyHints, FrontmatterSpan, parse_frontmatter}; +// Spec §3.3 의 surface 보존 정책 — explicit (NOT glob) 으로 future addition leak 방지. +pub use crate::normalize::{build_canonical_document, derive_title}; +pub use crate::types::{ + // 5 사용 type + ParsedBlock, ParsedBlockKind, ParsedPayload, Warning, WarningKind, + // 3 forward-declared struct (보존 — spec §3.3 + §11.5 future surface) + ParsedImageRegion, ParsedPdfPage, ParsedAudioSegment, +}; + /// Parser-version label for Markdown files ingested through this crate. /// Re-exported so `kebab-app::schema_with_config` can embed it in /// `SchemaV1.models.parser_version` without duplicating the literal. diff --git a/crates/kebab-normalize/src/lib.rs b/crates/kebab-parse-md/src/normalize.rs similarity index 96% rename from crates/kebab-normalize/src/lib.rs rename to crates/kebab-parse-md/src/normalize.rs index bc1e988..0f45d56 100644 --- a/crates/kebab-normalize/src/lib.rs +++ b/crates/kebab-parse-md/src/normalize.rs @@ -1,5 +1,8 @@ -//! `kb-normalize` — lift parser output (`kb-parse-types`) into a -//! [`kebab_core::CanonicalDocument`] with deterministic IDs. +//! `kb-parse-md::normalize` — lift parser output (`kb-parse-md::types`) +//! into a [`kebab_core::CanonicalDocument`] with deterministic IDs. +//! +//! v0.19.0 부터 kebab-parse-md 의 in-crate module (이전 별 crate +//! `kebab-normalize` — HOTFIXES.md 2026-05-26 참조). //! //! Per design §3.4 (CanonicalDocument / Block), §4.2 (ID recipe), §4.3 //! (ordinal rule), §3.6 (Provenance), §8 (module boundaries). @@ -8,14 +11,13 @@ //! //! * [`build_canonical_document`] — assemble a `CanonicalDocument` from //! `(RawAsset, Metadata, Vec, ParserVersion, Vec)`. -//! * [`id_for_doc`], [`id_for_block`] — re-exports of the canonical -//! ID-recipe functions in `kb-core::ids` (§4.2). `kb-core` is the only -//! implementation; `kb-normalize` is the canonical *entry point* per -//! design §8. +//! * `id_for_doc` / `id_for_block` — canonical ID-recipe functions in +//! `kb-core::ids` (§4.2). Callers should import them from `kebab_core` +//! directly (v0.19.0 흡수 후 re-export 제거 — production caller 0 +//! verified, spec §3.3 R10). //! -//! This crate must NOT depend on any parser implementation crate -//! (`kb-parse-md`, `kb-parse-pdf`, …). All parser output flows in via -//! the shared `kb-parse-types` crate. +//! Other parser crates (pdf / image / code) self-emit `CanonicalDocument` +//! and must NOT cross-import this module per design §8. use std::collections::HashMap; use std::path::Path; @@ -24,14 +26,12 @@ use anyhow::Result; use kebab_core::{ Block, BlockId, CanonicalDocument, CodeBlock, CommonBlock, DocumentId, HeadingBlock, ImageRefBlock, Inline, Lang, ListBlock, Metadata, ParserVersion, Provenance, ProvenanceEvent, - ProvenanceKind, RawAsset, TableBlock, TextBlock, + ProvenanceKind, RawAsset, TableBlock, TextBlock, id_for_block, id_for_doc, }; -use kebab_parse_types::{ParsedBlock, ParsedPayload, Warning, WarningKind}; +use crate::types::{ParsedBlock, ParsedPayload, Warning, WarningKind}; use time::OffsetDateTime; use unicode_normalization::UnicodeNormalization; -pub use kebab_core::{id_for_block, id_for_doc}; - /// Build a [`CanonicalDocument`] from the raw asset, frontmatter /// metadata, parser blocks, parser version, and any warnings. /// @@ -486,7 +486,7 @@ mod tests { let h1_b = vec!["B".to_string()]; vec![ ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Paragraph, + kind: crate::types::ParsedBlockKind::Paragraph, heading_path: h1_a.clone(), source_span: SourceSpan::Line { start: 1, end: 1 }, payload: ParsedPayload::Paragraph { @@ -495,7 +495,7 @@ mod tests { }, }, ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Paragraph, + kind: crate::types::ParsedBlockKind::Paragraph, heading_path: h1_a.clone(), source_span: SourceSpan::Line { start: 2, end: 2 }, payload: ParsedPayload::Paragraph { @@ -504,7 +504,7 @@ mod tests { }, }, ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Paragraph, + kind: crate::types::ParsedBlockKind::Paragraph, heading_path: h1_a.clone(), source_span: SourceSpan::Line { start: 3, end: 3 }, payload: ParsedPayload::Paragraph { @@ -513,7 +513,7 @@ mod tests { }, }, ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Code, + kind: crate::types::ParsedBlockKind::Code, heading_path: h1_a, source_span: SourceSpan::Line { start: 4, end: 5 }, payload: ParsedPayload::Code { @@ -522,7 +522,7 @@ mod tests { }, }, ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Paragraph, + kind: crate::types::ParsedBlockKind::Paragraph, heading_path: h1_b, source_span: SourceSpan::Line { start: 6, end: 6 }, payload: ParsedPayload::Paragraph { @@ -815,7 +815,7 @@ mod tests { fn audio_ref_block_skipped_with_warning() { let span = SourceSpan::Line { start: 1, end: 1 }; let blocks = vec![ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::AudioRef, + kind: crate::types::ParsedBlockKind::AudioRef, heading_path: vec![], source_span: span, payload: ParsedPayload::AudioRef { @@ -859,7 +859,7 @@ mod tests { let nfd_heading = "\u{1100}\u{1161}".to_string(); // 가 (NFD) let nfc_heading = "\u{AC00}".to_string(); // 가 (NFC) let mk_block = |heading: String| ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Paragraph, + kind: crate::types::ParsedBlockKind::Paragraph, heading_path: vec![heading], source_span: span.clone(), payload: ParsedPayload::Paragraph { @@ -1067,7 +1067,7 @@ mod tests { let mut metadata = fixture_metadata(); metadata.user.remove("title"); let blocks = vec![ParsedBlock { - kind: kebab_parse_types::ParsedBlockKind::Heading, + kind: crate::types::ParsedBlockKind::Heading, heading_path: vec![], source_span: span(), payload: ParsedPayload::Heading { diff --git a/crates/kebab-parse-types/src/lib.rs b/crates/kebab-parse-md/src/types.rs similarity index 91% rename from crates/kebab-parse-types/src/lib.rs rename to crates/kebab-parse-md/src/types.rs index 1b06c5c..75bce15 100644 --- a/crates/kebab-parse-types/src/lib.rs +++ b/crates/kebab-parse-md/src/types.rs @@ -1,4 +1,7 @@ -//! `kb-parse-types` — parser intermediate representations (§3.7b). +//! `kb-parse-md::types` — parser intermediate representations (§3.7b). +//! +//! v0.19.0 부터 kebab-parse-md 의 in-crate module (이전 별 crate +//! `kebab-parse-types` — HOTFIXES.md 2026-05-26 참조). //! //! Depends ONLY on `kb-core`. Must NOT depend on any parser library //! (`pulldown-cmark`, `pdf-extract`, `image`, `whisper-rs`, …) and must diff --git a/crates/kebab-parse-md/tests/blocks_snapshots.rs b/crates/kebab-parse-md/tests/blocks_snapshots.rs index 2de7bee..914240c 100644 --- a/crates/kebab-parse-md/tests/blocks_snapshots.rs +++ b/crates/kebab-parse-md/tests/blocks_snapshots.rs @@ -16,7 +16,7 @@ //! `[{"kind":"text","text":"…"},{"kind":"code","code":"…"}]`. use kebab_parse_md::parse_blocks; -use kebab_parse_types::{ParsedBlock, Warning}; +use kebab_parse_md::{ParsedBlock, Warning}; use serde::Serialize; use serde_json::Value; use std::fs; diff --git a/crates/kebab-parse-md/tests/frontmatter_snapshots.rs b/crates/kebab-parse-md/tests/frontmatter_snapshots.rs index da22976..ce9c6ae 100644 --- a/crates/kebab-parse-md/tests/frontmatter_snapshots.rs +++ b/crates/kebab-parse-md/tests/frontmatter_snapshots.rs @@ -20,7 +20,7 @@ use time::macros::datetime; struct Snapshot { metadata: kebab_core::Metadata, span_present: bool, - warnings: Vec, + warnings: Vec, } fn fixtures_dir() -> PathBuf { diff --git a/crates/kebab-normalize/tests/normalize_snapshot.rs b/crates/kebab-parse-md/tests/normalize_snapshot.rs similarity index 98% rename from crates/kebab-normalize/tests/normalize_snapshot.rs rename to crates/kebab-parse-md/tests/normalize_snapshot.rs index 65b04f6..27dab88 100644 --- a/crates/kebab-normalize/tests/normalize_snapshot.rs +++ b/crates/kebab-parse-md/tests/normalize_snapshot.rs @@ -19,8 +19,7 @@ use kebab_core::{ AssetId, AssetStorage, Checksum, MediaType, ParserVersion, RawAsset, SourceUri, WorkspacePath, }; -use kebab_normalize::build_canonical_document; -use kebab_parse_md::{BodyHints, parse_blocks, parse_frontmatter}; +use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter}; use serde_json::Value; use time::OffsetDateTime; diff --git a/crates/kebab-parse-types/Cargo.toml b/crates/kebab-parse-types/Cargo.toml deleted file mode 100644 index 4c7a42b..0000000 --- a/crates/kebab-parse-types/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "kebab-parse-types" -version = { workspace = true } -edition = { workspace = true } -rust-version = { workspace = true } -license = { workspace = true } -repository = { workspace = true } -description = "Parser intermediate representations (no parser libs allowed)" - -[dependencies] -kebab-core = { path = "../kebab-core" } -serde = { workspace = true } - -[lints] -workspace = true diff --git a/crates/kebab-store-sqlite/Cargo.toml b/crates/kebab-store-sqlite/Cargo.toml index 7322b61..be23855 100644 --- a/crates/kebab-store-sqlite/Cargo.toml +++ b/crates/kebab-store-sqlite/Cargo.toml @@ -29,13 +29,13 @@ thiserror = { workspace = true } [dev-dependencies] tempfile = "3" serde_json = { workspace = true } -# kb-parse-md / kb-normalize / kb-chunk are dev-only — used to build a -# CanonicalDocument + Vec 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. +# kb-parse-md / kb-chunk are dev-only — used to build a CanonicalDocument + +# Vec from a fixture in the contract round-trip test. kb-parse-md +# absorbed kb-normalize in v0.19.0 (HOTFIXES.md 2026-05-26). 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" } [lints] diff --git a/crates/kebab-store-sqlite/tests/contract_roundtrip.rs b/crates/kebab-store-sqlite/tests/contract_roundtrip.rs index ba56601..d6665c9 100644 --- a/crates/kebab-store-sqlite/tests/contract_roundtrip.rs +++ b/crates/kebab-store-sqlite/tests/contract_roundtrip.rs @@ -13,8 +13,7 @@ use kebab_core::{ AssetId, AssetStorage, Checksum, ChunkPolicy, ChunkerVersion, Chunker, DocumentStore, MediaType, ParserVersion, RawAsset, SourceUri, WorkspacePath, }; -use kebab_normalize::build_canonical_document; -use kebab_parse_md::{BodyHints, parse_blocks, parse_frontmatter}; +use kebab_parse_md::{BodyHints, build_canonical_document, parse_blocks, parse_frontmatter}; use kebab_store_sqlite::SqliteStore; use time::OffsetDateTime; diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e7d3101..df8acee 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -53,8 +53,6 @@ flowchart TB pimg["kebab-parse-image"] paud["kebab-parse-audio
(P8 보류)"] pcode["kebab-parse-code
(P10-1A-2 + P10-1B + P10-1C-Go + P10-1C-JK + P10-2 + P10-3 + P10-1D)"] - ptypes["kebab-parse-types"] - norm["kebab-normalize"] chunk["kebab-chunk"] end subgraph Persist ["persistence"] @@ -85,7 +83,6 @@ flowchart TB app --> pimg app --> paud app --> pcode - app --> norm app --> chunk app --> sqlite app --> vector @@ -96,12 +93,11 @@ flowchart TB app --> eval app --> config - pmd --> ptypes - ppdf --> ptypes - pimg --> ptypes - paud --> ptypes + pmd --> core + ppdf --> core + pimg --> core + paud --> core pcode --> core - norm --> ptypes embedlocal --> embed llmlocal --> llm rag --> search @@ -121,8 +117,6 @@ flowchart TB sqlite --> core vector --> core chunk --> core - norm --> core - ptypes --> core search --> core rag --> core srcfs --> core @@ -131,7 +125,7 @@ flowchart TB UI → store/llm/parse 직접 의존 금지. 모든 user-facing 진입은 `kebab-app` facade 만 통한다 (frozen 설계 §8). `kebab-cli` 가 `--config ` flag 를 honor 하려면 `kebab_app::*_with_config(cfg, …)` companion 을 통해 Config 을 명시적으로 thread 하는 패턴 — 자세한 이유는 [tasks/HOTFIXES.md](../tasks/HOTFIXES.md) 의 `--config` 항목. -`kebab-parse-code` 의 외부 tree-sitter grammar crate 의존: P10-1A-2 에서 `tree-sitter-rust` 추가, P10-1B 에서 `tree-sitter-python` / `tree-sitter-typescript` / `tree-sitter-javascript` 추가, P10-1C-Go 에서 `tree-sitter-go` 추가, P10-1C-JK 에서 `tree-sitter-java` / `tree-sitter-kotlin-ng` 추가, P10-1D 에서 `tree-sitter-c` / `tree-sitter-cpp` 추가. 모두 `kebab-parse-code` 에만 격리 (facade 룰 — UI crate / chunker 가 직접 import 금지). Kotlin 은 `tree-sitter-kotlin-ng` 사용 (bare `tree-sitter-kotlin` 은 tree-sitter 0.21–0.23 에 고착 — 사용 불가). v0.18.0+ 부터 `kebab-source-fs` 는 자체 `code_meta` 모듈 (lang detect + skip helpers + BUILTIN_BLACKLIST) 을 보유, kebab-parse-code 와 분리 (refactor 2026-05-26). +`kebab-parse-code` 의 외부 tree-sitter grammar crate 의존: P10-1A-2 에서 `tree-sitter-rust` 추가, P10-1B 에서 `tree-sitter-python` / `tree-sitter-typescript` / `tree-sitter-javascript` 추가, P10-1C-Go 에서 `tree-sitter-go` 추가, P10-1C-JK 에서 `tree-sitter-java` / `tree-sitter-kotlin-ng` 추가, P10-1D 에서 `tree-sitter-c` / `tree-sitter-cpp` 추가. 모두 `kebab-parse-code` 에만 격리 (facade 룰 — UI crate / chunker 가 직접 import 금지). Kotlin 은 `tree-sitter-kotlin-ng` 사용 (bare `tree-sitter-kotlin` 은 tree-sitter 0.21–0.23 에 고착 — 사용 불가). v0.18.0+ 부터 `kebab-source-fs` 는 자체 `code_meta` 모듈 (lang detect + skip helpers + BUILTIN_BLACKLIST) 을 보유, kebab-parse-code 와 분리 (refactor 2026-05-26). v0.19.0 부터 `kebab-parse-md` 가 `kebab-parse-types` (parser intermediate types) + `kebab-normalize` (CanonicalDocument lift) 두 crate 를 흡수 — 24 → 22 crates, design §3.7b 재작성 (HOTFIXES 2026-05-26). ## 디렉토리 구조 @@ -165,10 +159,9 @@ kebab/ │ ├── p8/p8-1, p8-2 # (2 — 보류) │ └── p9/p9-1 … p9-5 # (5) ├── crates/ -│ ├── kebab-core/ kebab-parse-types/ kebab-config/ # 도메인 + 설정 (P0) +│ ├── kebab-core/ kebab-config/ # 도메인 + 설정 (P0) │ ├── kebab-source-fs/ # 워크스페이스 walk + checksum (P1-1) -│ ├── kebab-parse-md/ # Markdown frontmatter + blocks (P1-2/3) -│ ├── kebab-normalize/ # ParsedBlock → CanonicalDocument (P1-4) +│ ├── kebab-parse-md/ # Markdown frontmatter + blocks + types + ParsedBlock → CanonicalDocument lift (P1-2/3/4 — v0.19.0 흡수) │ ├── kebab-chunk/ # heading-aware + pdf-page-v1 + code-*-ast-v1 (Tier 1) + k8s-manifest-resource-v1 + dockerfile-file-v1 + manifest-file-v1 + tier2_shared (P10-2) + code-text-paragraph-v1 (P10-3) chunker (P1-5, P7-2, P10-1A-2, P10-1B, P10-1C-Go, P10-1C-JK, P10-2, P10-3, P10-1D) │ │ └── src/ │ │ ├── code_*_ast_v1.rs # Tier 1 AST chunkers (rust/python/ts/js/go/java/kotlin/c/cpp) diff --git a/docs/superpowers/plans/2026-05-26-normalize-absorption-plan.md b/docs/superpowers/plans/2026-05-26-normalize-absorption-plan.md new file mode 100644 index 0000000..891c04f --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-normalize-absorption-plan.md @@ -0,0 +1,885 @@ +--- +status: open +target_version: 0.19.0 +spec: docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md +contract_sections: ["§3.7b", "§8"] +related_specs: + - docs/superpowers/specs/2026-04-27-kebab-final-form-design.md + - docs/superpowers/specs/2026-05-26-source-fs-dep-lightening-spec.md +sibling_plan: docs/superpowers/plans/2026-05-26-source-fs-dep-lightening-plan.md +--- + +# kebab-normalize + kebab-parse-types 흡수 — implementation plan + +> spec round 3 APPROVE 직후의 plan (revision 2 — 10 step → **15 step** decompose). spec §3.1-§3.10 의 결정 + §3.5/§3.6 의 design contract diff + §3.7 (a)-(g) callsite migration + §3.9 의 HOTFIXES wording + §5.1-§5.11 의 verification gate 가 step 단위로 분산. design + doc 갱신 (구 Step 9) 을 4 step (§3.7b 재작성 / §8 graph / ARCHITECTURE + INDEX + HOTFIXES + HANDOFF / version 사이드카 verify) 으로 split — critic + verifier 의 closure granularity 향상. + +## §0 Pre-flight + branch state + +- **Branch**: `refactor/normalize-absorption` (현재 위치, main `d4395a3` 위에서 분기 완료). +- **Base SHA**: `d4395a3` (PR #185 sibling — source-fs dep lightening 머지 직후, v0.18.0 cut 완료 시점). +- **Working dir**: `/home/altair823/kebab`. +- **Env 강제** (CLAUDE.md disk-protection — `~/.claude/CLAUDE.md` 의 "Disk Layout — 루트 디스크 보호가 최우선" 룰): + - `export CARGO_TARGET_DIR=/build/out/cargo-target/target` — 본 plan 의 모든 cargo 명령에 적용. `target/` 가 repo root 아래에 생성되지 않게 (16 GB RAM 머신 의 `/` 250 G 보호). + - `export TMPDIR=/build/cache/tmp` — 대용량 임시 파일 발생 시 보호. +- **Cargo build 직렬화** (CLAUDE.md "Build / test / lint" + MEMORY.md `feedback_serial_build_only.md`): + - 모든 cargo 명령 `-j 1` 강제. 18 integration-test binary 동시 link 시 OOM (linker SIGKILL). + - per-crate `cargo test -p ` 는 `-j 1` 없어도 OK 이나 일관성을 위해 명시. + - cargo test / clippy / build 동시 background 실행 금지. 하나 끝난 후 다음. +- **`target/` clean policy** (CLAUDE.md 룰 + spec §5.10): full workspace test 직전 `cargo clean` 1회 (Step 15). 중간 step (Step 2-14) 에서는 per-crate build 만 — `cargo clean` 불필요 (incremental cache 활용). +- **HOTFIXES.md / HANDOFF.md / README.md 변경 0** (spec §7 명시) — HOTFIXES.md 와 HANDOFF.md 는 본 plan 의 Step 14 에서 추가 (README 는 변경 0). +- **4 frozen task spec (p1-2, p1-3, p1-4, p9-fb-07) 변경 0** + ~25 referencing task spec mechanical update 0 (spec §2 + §5.7). +- **wire schema 변경 0** (spec §1.9 verified, 16 wire schema 0 hit). +- **workspace `Cargo.toml` version bump 0.18.0 → 0.19.0** (Step 10 Hunk (b)). + +## §1 Approach summary + +Spec §3 의 핵심 sequencing — destination = `kebab-parse-md` (spec §3.1, Option A): + +1. **신규 module 부터 작성** (Step 2-3) — `kebab-parse-md/src/types.rs` (parse-types 98 LOC 1:1 이식) + `kebab-parse-md/src/normalize.rs` (normalize 의 production fn body 이식, 4 hard-coded agent literal + tracing target literal 모두 보존). +2. **`lib.rs` 에 module + pub explicit re-export 등록** (Step 4) — 5 사용 type + 3 forward-declared struct + `build_canonical_document` / `derive_title` 의 surface 보존 (spec §3.3). explicit (glob 아님) — Q3 closure. +3. **`blocks.rs` + `frontmatter.rs` use 갱신** (Step 5) — `use kebab_parse_types::*` → `use crate::types::*`. 동일 crate 내 in-source ref shift. +4. **`kebab-app` callsite + dep cleanup** (Step 6-7, atomic 2 step) — `lib.rs:51` use statement + `lib.rs:1119` context string (Step 6) → `Cargo.toml` 의 2 dep 제거 (Step 7, `kebab-normalize` regular + `kebab-parse-types` dead regular — spec §3.10 incidental cleanup). +5. **dev-dep migration** (Step 8) — `kebab-chunk` + `kebab-store-sqlite` 의 `kebab-normalize` dev-dep 제거 (이미 `kebab-parse-md` dev-dep 보유). 통합 test source 의 `use kebab_normalize::*;` → `use kebab_parse_md::*;`. +6. **test file 이동 + 자기 참조 verify** (Step 9) — `kebab-normalize/tests/normalize_snapshot.rs` → `kebab-parse-md/tests/normalize_snapshot.rs`. 자기 참조 dev-dep declare 없는 cargo standard behavior verify. +7. **workspace `Cargo.toml` 갱신 — anchor step** (Step 10) — Hunk (a) `members` 의 2 entry 삭제 + Hunk (b) `workspace.package.version` 0.18.0 → 0.19.0 (NIT #N6 closure). +8. **`kebab-normalize/` + `kebab-parse-types/` 디렉토리 삭제** (Step 11) — `git rm -r`. workspace 가 22 crate 로 collapse. +9. **design §3.7b 재작성** (Step 12) — spec §3.5 의 4-단락 wording 으로 design contract 의 line 703-764 replace. +10. **design §8 graph 갱신** (Step 13) — spec §3.6 의 diff (3 edge 제거 + 2 forbidden bullet 의미 갱신 + commentary) 적용. +11. **ARCHITECTURE + INDEX + HOTFIXES + HANDOFF 갱신** (Step 14) — 4 doc 의 mechanical update. INDEX.md 의 "Future work / deferred" 섹션 신설 (현재 부재 — §11.7). +12. **workspace 회귀 + clean commit — closure** (Step 15) — `cargo clean` + 7 cargo gate + wire diff verify + 1 clean commit. + +핵심 ordering invariant: + +- **Step 2-3 < Step 4-5**: destination module file 생성 후 lib.rs re-export — 둘 동시 commit 시 cargo build green 보장. +- **Step 4-5 < Step 6-7**: in-crate ref shift 후 kebab-app callsite + dep — 외부 caller redirect 완료 후 dep 제거. +- **Step 6-7 < Step 8**: kebab-app 정합 후 dev-dep migration — order independent 이나 sequential gate 분리. +- **Step 8 < Step 9**: dev-dep cleanup 후 test file 이동 — git mv 가 git 의 add/remove 동시 commit 시 file content 보존. +- **Step 6-9 < Step 10**: 모든 caller redirect 후 workspace.members 삭제 — 중간 build 깨짐 방지. +- **Step 10 < Step 11**: workspace.members 제거 후 디렉토리 삭제 — stale path 회피. +- **Step 11 < Step 12-14**: production code 갱신 완료 후 design contract + doc 갱신 — reality ≡ contract. +- **Step 12-14 < Step 15**: 모든 doc 갱신 후 회귀 + commit — single clean commit 의 일관성. + +## §2 Steps (15 steps) + +### Step 1: Pre-flight baseline 측정 + env 확인 + +- **Files affected**: 변경 0 (측정 only). +- **Action**: + - `cd /home/altair823/kebab && git rev-parse HEAD` → `d4395a3` 또는 그 위 commit 확인 (refactor branch 의 base). + - env 확인: `echo $CARGO_TARGET_DIR` 가 `/build/out/cargo-target/target` 인지. 비어있으면 §0 의 export 적용. + - workspace baseline count 측정: `cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length'` → **24** (현재 시점). + - dead `kebab-parse-types` regular dep verify (spec §3.10): `grep -n "kebab-parse-types" crates/kebab-app/Cargo.toml` → 1 hit (line 16 부근), `grep -rn "kebab_parse_types" crates/kebab-app/src/` → 0 hit. + - baseline test count 측정 + persist (MAJOR GAP3 closure — spec §5.1 의 1313 + α 의 unit 정합화: test *함수 수* sum, NOT *binary 수* 행수): + ```bash + $ mkdir -p .omc/state + $ cargo test --workspace --no-fail-fast -j 1 2>&1 \ + | awk '/^test result: ok\./ {for(i=1;i<=NF;i++) if($i=="passed;") sum += $(i-1)} END {print sum}' \ + > .omc/state/normalize-absorption-baseline.txt + $ cat .omc/state/normalize-absorption-baseline.txt + 1313 # 예상 (spec §5.1) + ``` + 본 file 은 Step 15 의 numeric compare gate 의 source-of-truth. `.omc/state/` 는 git untracked (gitignore default 또는 `.omc/.gitignore`). +- **Exit gate (cargo cli — falsifiable / observable / idempotent / scope-correct)**: + - `cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length'` = **24** (observable, idempotent). + - `cargo build --workspace -j 1 2>&1 | tail -5` 의 마지막 라인 = `Finished` 또는 `Compiling` (현 시점 baseline green — falsifiable). +- **Spec 참조**: §5.1 (baseline), §3.10 (dead dep verify). + +### Step 2: 신규 `crates/kebab-parse-md/src/types.rs` 생성 (98 LOC 1:1 이식) + +- **Files affected**: + - `crates/kebab-parse-md/src/types.rs` (신규, ≈ 98 LOC, byte-identical 이식). +- **Action**: + - `cp crates/kebab-parse-types/src/lib.rs crates/kebab-parse-md/src/types.rs` 또는 Write tool 로 신규 생성. + - 본문 = `kebab-parse-types/src/lib.rs` 의 98 LOC 의 1:1 이식: + - 5 사용 type (`ParsedBlock` + `ParsedBlockKind` + `ParsedPayload` + `Warning` + `WarningKind`) 의 serde 표현 + variant 명 byte-identical 보존 (spec §2.2 의 non-goal — 의미 변경 0). + - 3 forward-declared struct (`ParsedImageRegion` + `ParsedPdfPage` + `ParsedAudioSegment`) 보존 (spec §3.3 + §11.5 의 future surface). + - module-level doc 의 §3.7b reference 보존 + 한 줄 추가: `//! v0.19.0 부터 kebab-parse-md 의 in-crate module (이전 별 crate kebab-parse-types — HOTFIXES.md 2026-05-26 참조).` + - **본 step 은 *추가* only** — 기존 `crates/kebab-parse-types/` 는 아직 alive. lib.rs 의 mod declare 가 없으므로 cargo 가 `types.rs` 무시 → build 깨지지 않음. +- **Exit gate**: + - `wc -l crates/kebab-parse-md/src/types.rs` ≈ 98 (≤ 105) — falsifiable. + - `grep -c "ParsedBlock\|ParsedBlockKind\|ParsedPayload\|Warning\|WarningKind\|ParsedImageRegion\|ParsedPdfPage\|ParsedAudioSegment" crates/kebab-parse-md/src/types.rs` ≥ 8 — 8 type 모두 등장. + - `cargo build -p kebab-parse-md -j 1 2>&1 | tail -3` 의 마지막 라인 = `Finished` (mod declare 없으므로 file 무시). +- **Spec 참조**: §3.2 (module placement), §3.3 (visibility), §11.5 (future surface 보존). + +### Step 3: 신규 `crates/kebab-parse-md/src/normalize.rs` 생성 + `Cargo.toml` dep 갱신 (1097 LOC 이식 + literal 보존 + dep migration) + +- **Files affected**: + - `crates/kebab-parse-md/src/normalize.rs` (신규, ≈ 1097 LOC, byte-identical 이식 + literal 보존 정책). + - `crates/kebab-parse-md/Cargo.toml` (dep 갱신 — CRITICAL #1 + GAP2 closure). +- **Action**: + - **(a) normalize.rs 생성** — `cp crates/kebab-normalize/src/lib.rs crates/kebab-parse-md/src/normalize.rs` 또는 Write 로 신규 생성. + - 본문 = `kebab-normalize/src/lib.rs` 의 1097 LOC 의 production fn + comment + cfg(test) unit tests 1:1 이식: + - `build_canonical_document` (signature `(asset: &RawAsset, metadata: Metadata, blocks: Vec, parser_version: &ParserVersion, warnings: Vec) -> Result`) 의 production body — spec §1.5 의 actual source byte-identical. + - `derive_title(frontmatter_title: &str, blocks: &[Block], file_stem: &str) -> String` — spec §1.5 의 `blocks: &[Block]` (lifted, NOT ParsedBlock) inline 주석 보존. + - `warning_agent(kind: &WarningKind) -> &'static str` 의 4 variant `"kb-parse-md"` 단일 return body 보존 (spec §3.7e + NEW MAJOR #N2 closure). + - **(b) literal 보존 (CRITICAL — spec §3.7 (e)(g) + §1.9 의 6-row trace table)**: + | line (post-port) | string | 보존? | + |---|---|---| + | `:109` (approx) | `target: "kebab-normalize"` (tracing::debug! literal) | ★ 보존 | + | `:122` (approx) | `"kb-source-fs"` (Discovered event agent) | 보존 | + | `:128` (approx) | `"kb-parse-md"` (Parsed event agent) | 보존 | + | `:134` (approx) | `"kb-normalize"` (Normalized event agent) | ★ 보존 | + | `:143` (approx) | `warning_agent(&w.kind).to_string()` (Warning event agent — 동적, "kb-parse-md" 단일 return) | 보존 | + | `:153` (approx) | `"kb-normalize"` (lift_warnings event agent) | ★ 보존 | + - **(c) in-source ref shift — 3 갈래** (actual `crates/kebab-normalize/src/lib.rs` grep 결과 기반): + ```diff + -use kebab_parse_types::{ParsedBlock, ParsedPayload, Warning, WarningKind}; + +use crate::types::{ParsedBlock, ParsedPayload, Warning, WarningKind}; + ``` + 추가로 cfg(test) mod tests 안의 **9 hit fully-qualified call** 도 갱신 (lib.rs:489, :498, :507, :516, :525, :818, :862, :1070 — 모두 `kebab_parse_types::ParsedBlockKind::*` 패턴): + ```diff + -kind: kebab_parse_types::ParsedBlockKind::Paragraph, + +kind: crate::types::ParsedBlockKind::Paragraph, + ``` + `sed -i 's/kebab_parse_types::/crate::types::/g' crates/kebab-parse-md/src/normalize.rs` (or Edit `replace_all`) — 9 hit 모두 mechanical 변경. + - **(d) `pub use kebab_core::{id_for_block, id_for_doc}` 제거 + in-body call 의 unqualified import 보존** (CRITICAL #2 closure): + actual `lib.rs:33` 의 `pub use` 는 *re-export + 동시 current module scope import*. line 67 (`id_for_doc(...)`) + line 241 (`id_for_block(...)`) 의 unqualified call 이 `pub use` 의 scope import 에 의존. `pub use` 만 제거 시 unqualified call unresolved → compile error. + ```diff + -pub use kebab_core::{id_for_block, id_for_doc}; + +// (re-export 제거 — spec §3.3 R10 decision; production caller 0 verified. + +// in-body unqualified call 은 아래 use block 의 import 로 대체.) + ``` + 그리고 같은 file 의 *기존* `use kebab_core::{...}` block 에 `id_for_block, id_for_doc` 추가 (실제 use block 의 정확한 위치는 actual `lib.rs` 의 `use kebab_core::{` block 검색 후 정정 — Block / Metadata / SourceSpan 등의 normal import 와 함께 묶음): + ```diff + use kebab_core::{ + Block, BlockId, ..., + + id_for_block, id_for_doc, // ← pub use 제거 후 in-body unqualified call 보존 (line 67, 241) + ... + }; + ``` + - **(e) `crates/kebab-parse-md/Cargo.toml` dep 갱신** (CRITICAL #1 + GAP2 closure — spec §3.4): + ```diff + [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 } + +# 흡수된 kb-normalize 의 NFKC 의존 — actual kebab-normalize/src/lib.rs:31 의 + +# `use unicode_normalization::UnicodeNormalization;` 이 normalize.rs 이식 시 동반. + +# 이미 kebab-app 도 사용 중인 0.1 major (version drift 0). + +unicode-normalization = "0.1" + pulldown-cmark = { version = "0.13", default-features = false } + ... + ``` + - **본 step 도 *추가* only** — 기존 `crates/kebab-normalize/` 는 아직 alive. lib.rs 의 mod declare (Step 4) 가 없으므로 normalize.rs file 무시되어 build 영향 0. 단 (e) 의 Cargo.toml 갱신은 Step 5/6 의 use shift 직후 fully active. +- **Exit gate**: + - `wc -l crates/kebab-parse-md/src/normalize.rs` ≈ 1097 (≤ 1110). + - `grep -c 'target: "kebab-normalize"' crates/kebab-parse-md/src/normalize.rs` = 1 — tracing target literal 보존 (spec §3.7 (g) + R8 mitigation). + - production agent literal 의 정확한 검증 (MINOR GAP6 — production body 의 `agent: "kb-..."` 패턴 grep): `grep -cE 'agent:\s*"kb-(source-fs|parse-md|normalize)"\.to_string\(\)' crates/kebab-parse-md/src/normalize.rs` ≥ 3 — 3 hard-coded literal (Discovered + Parsed + Normalized) 의 production body emission. warning_agent body 의 4 `"kb-parse-md"` return + lift_warnings 의 `"kb-normalize"` 합쳐 production agent literal 5 hit 보장. + - `grep -c "use crate::types::" crates/kebab-parse-md/src/normalize.rs` ≥ 1 — in-source ref shift. + - `grep -c "kebab_parse_types::" crates/kebab-parse-md/src/normalize.rs` = 0 — fully-qualified 9 hit 모두 갱신 (MAJOR #5 closure 의 normalize.rs 쪽). + - `grep -c "pub use kebab_core::" crates/kebab-parse-md/src/normalize.rs` = 0 — re-export 제거 (spec §3.3 R10). + - `grep -E "use kebab_core::\{" crates/kebab-parse-md/src/normalize.rs | grep -c "id_for_block\|id_for_doc"` ≥ 1 — id_for_* unqualified import 보존 (CRITICAL #2 closure). + - `grep -c "kebab-parse-types" crates/kebab-parse-md/Cargo.toml` = 0 — Cargo.toml dep 제거. + - `grep -c "unicode-normalization" crates/kebab-parse-md/Cargo.toml` ≥ 1 — Cargo.toml dep 추가 (CRITICAL #1 + GAP2 closure). + - `cargo build -p kebab-parse-md -j 1` green (mod declare 없으므로 normalize.rs file 무시. Cargo.toml dep 만 active). +- **Spec 참조**: §3.2 (module placement), §3.3 (visibility), §3.4 (Cargo.toml dep diff), §3.7 (c)(d)(e)(g) (in-source ref + id_for_* + literal), §1.5 (signature), §1.9 (6-row trace), §3.3 R10 (id_for_*), CRITICAL #1 + #2 + NEW MAJOR #N1 + #N2 + MINOR GAP6 closure. + +### Step 4: `crates/kebab-parse-md/src/lib.rs` 갱신 (module declare + pub explicit re-export) + +- **Files affected**: `crates/kebab-parse-md/src/lib.rs`. +- **Action**: + - module-level doc 의 마지막에 한 줄 추가: + ```rust + //! v0.19.0 부터 `types` + `normalize` module 은 in-crate 흡수 + //! (`kebab-parse-types` + `kebab-normalize` 의 historical crate 가 본 crate 로 + //! collapse — see HOTFIXES.md 2026-05-26). + ``` + - module declare + pub explicit re-export 추가 (spec §3.3 + §4.2 Q3 — glob 아님): + ```rust + mod types; + mod normalize; + + // Spec §3.3 의 surface 보존 정책 — explicit (NOT glob) 으로 future addition leak 방지. + pub use crate::types::{ + // 5 사용 type + ParsedBlock, ParsedBlockKind, ParsedPayload, + Warning, WarningKind, + // 3 forward-declared struct (보존 — spec §3.3 + §11.5 future surface) + ParsedImageRegion, ParsedPdfPage, ParsedAudioSegment, + }; + pub use crate::normalize::{build_canonical_document, derive_title}; + ``` +- **Exit gate**: + - `grep -c "^mod types;\|^mod normalize;" crates/kebab-parse-md/src/lib.rs` = 2. + - `grep "pub use crate::types" crates/kebab-parse-md/src/lib.rs | grep -c "ParsedImageRegion\|ParsedPdfPage\|ParsedAudioSegment"` ≥ 1 — 3 forward-declared struct 의 explicit re-export 확인. + - `grep "pub use crate::normalize" crates/kebab-parse-md/src/lib.rs | grep -c "build_canonical_document\|derive_title"` ≥ 1. + - `cargo build -p kebab-parse-md -j 1` green — 모듈 활성화 후 cargo 가 types.rs + normalize.rs 컴파일. + - `cargo test -p kebab-parse-md -j 1` green — 이식된 normalize unit test (~700 LOC) 도 in-crate 통과. +- **Spec 참조**: §3.3 (visibility 정책), §4.2 Q3 (explicit vs glob — explicit 선택), §11.5 (future surface 보존). + +### Step 5: `crates/kebab-parse-md/src/{blocks,frontmatter}.rs` + `tests/{blocks_snapshots,frontmatter_snapshots}.rs` ref shift + +- **Files affected**: + - `crates/kebab-parse-md/src/blocks.rs` (4 hit — actual line 1, 25, 37, 1589 verified). + - `crates/kebab-parse-md/src/frontmatter.rs` (1+ hit — actual line 22 verified). + - `crates/kebab-parse-md/tests/blocks_snapshots.rs` (BLOCKER #1 — actual line 19 verified). + - `crates/kebab-parse-md/tests/frontmatter_snapshots.rs` (BLOCKER #1 — actual line 23 verified). +- **Action**: + - **(a) blocks.rs — file-wide replace (MAJOR #5 closure, 4 hit)**: + - line 1 doc comment: `//! Markdown body → flat \`Vec\` (§3.4 / §3.7b).` → `//! Markdown body → flat \`Vec\` (§3.4 / §3.7b).` + - line 25 doc-link: `[\`kebab_parse_types::ParsedPayload::ImageRef\`]` → `[\`crate::types::ParsedPayload::ImageRef\`]` + - line 37 use: `use kebab_parse_types::*;` (또는 explicit list) → `use crate::types::*;` (또는 explicit list 갱신) + - line 1589 production fully-qualified call: `kebab_parse_types::ParsedBlockKind::List` → `crate::types::ParsedBlockKind::List` + - Edit tool 의 `replace_all: true` 로 `kebab_parse_types::` → `crate::types::` 단일 치환 가능 (4 hit + 가능한 hidden hit 모두 mechanical 갱신). + - **(b) frontmatter.rs — 동일 패턴**: + ```diff + -use kebab_parse_types::{Warning, WarningKind}; + +use crate::types::{Warning, WarningKind}; + ``` + `MalformedFrontmatter` variant 의 fully-qualified call 등 추가 hit 검색 (`grep -n "kebab_parse_types" crates/kebab-parse-md/src/frontmatter.rs` 으로 확인 후 `replace_all`). + - **(c) tests/blocks_snapshots.rs (BLOCKER #1 — integration test 는 `crate::` 사용 불가, `kebab_parse_md::*` 사용)**: + ```diff + -use kebab_parse_types::{ParsedBlock, Warning}; + +use kebab_parse_md::{ParsedBlock, Warning}; + ``` + line 19 의 use 갱신. integration test 는 자기 crate `lib` 자동 link → `kebab_parse_md::*` re-export 활성화 (Step 4 의 lib.rs explicit re-export 의 8 type 중 4 type 활용). + - **(d) tests/frontmatter_snapshots.rs (BLOCKER #1)**: + ```diff + -warnings: Vec, + +warnings: Vec, + ``` + line 23 의 fully-qualified type 갱신 (function signature param). 추가 hit 검색 (`grep -n "kebab_parse_types" crates/kebab-parse-md/tests/frontmatter_snapshots.rs`) 후 replace_all. +- **Exit gate**: + - `grep -rn "kebab_parse_types" crates/kebab-parse-md/` = **0 hit** (src/ + tests/ 모두 — verify cmd 의 src/ 한정 해제, BLOCKER #1 closure). + - `grep -c "crate::types::" crates/kebab-parse-md/src/blocks.rs` ≥ 4 (line 1/25/37/1589 갱신). + - `grep -c "kebab_parse_md::" crates/kebab-parse-md/tests/blocks_snapshots.rs crates/kebab-parse-md/tests/frontmatter_snapshots.rs` ≥ 2. + - `cargo build -p kebab-parse-md -j 1` green. + - `cargo test -p kebab-parse-md -j 1` green (integration test 의 fixture builder 가 self-link 통해 redirect). +- **Spec 참조**: §3.7 (c) callsite migration (in-source ref shift), BLOCKER #1 + MAJOR #5 closure. + +### Step 6: `crates/kebab-app/src/lib.rs:51, :1119` callsite migration + +- **Files affected**: `crates/kebab-app/src/lib.rs`. +- **Action**: + - **(a) line 51 use statement** (spec §3.7 (a)): + ```diff + -use kebab_normalize::build_canonical_document; + +use kebab_parse_md::build_canonical_document; + ``` + - **(b) line 1119 context string** (spec §3.7 (b) + MAJOR #4 closure): + ```diff + - .context("kb-normalize::build_canonical_document")?; + + .context("kb-parse-md::build_canonical_document")?; + ``` + *주의*: `kb-parse-md::parse_frontmatter` (line 1091) + `kb-parse-md::parse_blocks` (line 1099) 의 context string 은 변경 없음 — byte-identical hunk 적용 금지 (MAJOR #4 의 closure 명시). +- **Exit gate**: + - `grep -n "kebab_normalize" crates/kebab-app/src/lib.rs` = 0 hit (use 와 의 자취 모두 사라짐). + - `grep -n "kb-normalize::" crates/kebab-app/src/lib.rs` = 0 hit (context string 갱신). + - `cargo build -p kebab-app -j 1` green — kebab-parse-md 의 re-export 가 alive 인 상태에서 redirect. + - `cargo test -p kebab-app -j 1` green. +- **Spec 참조**: §3.7 (a)(b), MAJOR #4 closure. + +### Step 7: `crates/kebab-app/Cargo.toml` dep cleanup (2 dep 제거 — regular + dead incidental) — **closure pre-pivot 1** + +- **Files affected**: `crates/kebab-app/Cargo.toml`. +- **Action** (spec §3.4 + §3.10): + MINOR #3 closure — hunk 적용 전 actual context 확인 (sed 으로 idempotent): + ```bash + $ sed -n '11,20p' crates/kebab-app/Cargo.toml # context 의 actual line 확인 + ``` + hunk: + ```diff + 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-normalize` regular dep 제거 — Step 6 의 use shift 가 정합되어 더 이상 dep 불필요. + - `kebab-parse-types` regular dep 제거 — *dead dep* (spec §3.10 incidental cleanup — Step 1 의 verify 에서 `kebab_parse_types` source import 0 hit 검증 완료). +- **Exit gate**: + - `grep -E "kebab-normalize|kebab-parse-types" crates/kebab-app/Cargo.toml` = 0 hit. + - `cargo build -p kebab-app -j 1` green. + - `cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)"` = 0 줄 — spec §5.2 의 anchor invariant. +- **Spec 참조**: §3.4 (Cargo.toml diff), §3.10 (incidental cleanup), §5.2 (anchor invariant). + +### Step 8: `crates/kebab-chunk/Cargo.toml` + `crates/kebab-store-sqlite/Cargo.toml` dev-dep migration + 통합 test source `use` 갱신 + +- **Files affected**: + - `crates/kebab-chunk/Cargo.toml`. + - `crates/kebab-store-sqlite/Cargo.toml`. + - `crates/kebab-chunk/tests/*.rs` (use statement shift). + - `crates/kebab-store-sqlite/tests/*.rs` (use statement shift). +- **Action**: + - **(a) `crates/kebab-chunk/Cargo.toml`** — spec §3.4: + ```diff + [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 } + ``` + 그리고 doc comment 의 `kb-parse-md / kb-normalize / kb-parse-code` mention 갱신 — `kb-parse-md / kb-parse-code` (kb-normalize 흡수 명시). + - **(b) `crates/kebab-store-sqlite/Cargo.toml`** — spec §3.4: + ```diff + kebab-parse-md = { path = "../kebab-parse-md" } + -kebab-normalize = { path = "../kebab-normalize" } + kebab-chunk = { path = "../kebab-chunk" } + ``` + - **(c) 통합 test source 의 use statement 갱신** — actual grep 결과 명시 (MAJOR #2 closure, 2 file:line): + - `crates/kebab-chunk/tests/long_section_snapshot.rs:21` — `use kebab_normalize::build_canonical_document;` → `use kebab_parse_md::build_canonical_document;` + - `crates/kebab-store-sqlite/tests/contract_roundtrip.rs:16` — `use kebab_normalize::build_canonical_document;` → `use kebab_parse_md::build_canonical_document;` + 추가 hit 검색 (`grep -rn "kebab_normalize" crates/kebab-chunk/tests/ crates/kebab-store-sqlite/tests/`) — 본 시점 의 verified hit = 2건. 새로 발견 시 동일 패턴으로 갱신. +- **Exit gate**: + - `grep -l "kebab-normalize" crates/kebab-chunk/Cargo.toml crates/kebab-store-sqlite/Cargo.toml` = 0 line. + - `grep -rn "kebab_normalize" crates/kebab-chunk/tests/ crates/kebab-store-sqlite/tests/` = 0 hit. + - `cargo test -p kebab-chunk -j 1` green — snapshot integration test 의 fixture builder 가 destination 으로 redirect. + - `cargo test -p kebab-store-sqlite -j 1` green — contract round-trip test. +- **Spec 참조**: §3.4 (Cargo.toml diff), MAJOR #11 closure. + +### Step 9: test file 이동 (`kebab-normalize/tests/` → `kebab-parse-md/tests/`) + 자기 참조 verify + +- **Files affected** (MAJOR #3 closure — actual `find` 결과 명시): + - `crates/kebab-normalize/tests/normalize_snapshot.rs` → `crates/kebab-parse-md/tests/normalize_snapshot.rs`. + - actual `find crates/kebab-normalize/tests/ -type f` 결과 = **`normalize_snapshot.rs` 단일 file** (sibling snapshots/ 또는 fixtures/ 서브디렉토리 부재). 본 step 의 mv 대상 = 1 file only. +- **Action**: + - **(a) git mv 로 mechanical move**: + ```bash + # 실 실행 전 사전 verify (idempotent + observable): + $ find crates/kebab-normalize/tests/ -type f + crates/kebab-normalize/tests/normalize_snapshot.rs + + # 단일 file mv (sibling fixture 부재 확인됨): + $ git mv crates/kebab-normalize/tests/normalize_snapshot.rs crates/kebab-parse-md/tests/normalize_snapshot.rs + ``` + - **(b) test file 의 use statement 갱신**: + - 기존 `use kebab_normalize::*;` 또는 `use kebab_normalize::{build_canonical_document, derive_title};` → `use kebab_parse_md::{build_canonical_document, derive_title};` 또는 in-crate `crate::*` 사용. + - `use kebab_parse_md::*;` 가 이미 alive 이면 keep (자기 crate import 는 integration test 에서 cargo standard behavior 로 자동 link — spec §3.7 (f), R3 / Q4 closure). + - `kebab_parse_types::*` 의 import 도 `kebab_parse_md::*` re-export 로 갈음. + - **(c) 자기 참조 dev-dep declare 제거 verify**: 본 step 이후 `kebab-normalize/` 디렉토리 자체가 Step 11 에서 삭제되므로 그 안의 Cargo.toml 의 `kebab-parse-md = { path = "..." }` dev-dep 도 vanish. 새 destination `kebab-parse-md/Cargo.toml` 에 *자기 참조* dev-dep 을 add 하지 않음 — cargo standard behavior (integration test 가 lib 자동 link). +- **Exit gate**: + - `ls crates/kebab-parse-md/tests/normalize_snapshot.rs` 존재. + - `ls crates/kebab-normalize/tests/normalize_snapshot.rs 2>&1 | grep -c "No such"` = 1 (이동 완료). + - `cargo test -p kebab-parse-md --test normalize_snapshot -j 1` green — spec §3.7 (f) 의 cargo standard behavior verified (R3/Q4 closure). + - `grep "kebab-parse-md = " crates/kebab-parse-md/Cargo.toml` = 0 hit (자기 참조 add 안 했는지 verify). +- **Spec 참조**: §3.7 (f) (test file 이동 + cargo standard behavior), R3 (Q4 closure). + +### Step 10: workspace `Cargo.toml` Hunk (a) members + Hunk (b) version — anchor step + +- **Files affected**: `Cargo.toml` (workspace root). +- **Action** (spec §3.8 + NIT #N6 closure — 2 hunk 분리): + - **Hunk (a) — `[workspace] members` 의 2 entry 삭제**: + ```diff + [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 변경**: + ```diff + [workspace.package] + edition = "2024" + rust-version = "1.85" + license = "MIT OR Apache-2.0" + repository = "https://github.com/altair823/org/kebab" + -version = "0.18.0" + +version = "0.19.0" # frozen design contract (§3.7b 재작성) 변경 trigger — CLAUDE.md "Release / binary version bump" + ``` + - 두 hunk 는 sequential 또는 parallel 적용 가능 — line context 가 충분히 분리되어 있음 (NIT #N6 의 의도). +- **Exit gate**: + - `cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length'` = **22** (spec §5.2 의 robust 명령). + - `grep '^version' Cargo.toml | head -1` = `version = "0.19.0"`. + - `cargo build --workspace -j 1` **green** — Step 6-9 가 모든 dep path 참조 제거 완료한 상태이므로, members 에서 제외된 두 orphan 디렉토리 (`crates/kebab-normalize/` + `crates/kebab-parse-types/`) 는 cargo 가 silently ignore. Cargo.lock 의 `[[package]]` entry 는 Step 11 직후 first `cargo build` 에서 자동 cleanup (MAJOR #1 closure — self-contradictory wording 제거). +- **Spec 참조**: §3.8 (Hunk a + b), NIT #N6 closure, §5.2 (workspace count invariant). + +### Step 11: `crates/kebab-normalize/` + `crates/kebab-parse-types/` 디렉토리 삭제 + +- **Files affected**: + - `crates/kebab-normalize/` (전체 디렉토리). + - `crates/kebab-parse-types/` (전체 디렉토리). +- **Action**: + - `git rm -r crates/kebab-normalize/` + - `git rm -r crates/kebab-parse-types/` + - `cargo build --workspace -j 1` 의 자동 Cargo.lock cleanup 으로 두 crate 의 `[[package]]` entry 사라짐 (Step 15 의 verification). +- **Exit gate**: + - `ls crates/kebab-normalize/ 2>&1 | grep -c "No such"` = 1. + - `ls crates/kebab-parse-types/ 2>&1 | grep -c "No such"` = 1. + - `ls -d crates/*/ | wc -l` = **22** (spec §5.2 의 secondary robust cmd). + - `cargo build --workspace -j 1` green — 모든 dep 정합. +- **Spec 참조**: §3.8 (디렉토리 삭제). + +### Step 12: design §3.7b 4-단락 재작성 (`docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` line 703-764) + +- **Files affected**: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` (§3.7b 부분). +- **Action**: + - line 703-764 의 §3.7b 본문을 spec §3.5 의 wording 으로 **replace** (strike 아닌 재작성). + - 4-단락 구조: + 1. **원래 의도** (v0.1~v0.18 머지 시점) — thin layer + medium-agnostic lift. + 2. **현재 상태 (v0.19.0~)** — 흡수 근거 (4 parser 중 1개만 lift 경유, fan-in/fan-out 모두 1). + 3. **보존된 surface** — 5 사용 type + 3 forward-declared struct → `kebab-parse-md` 의 `pub` re-export. + 4. **future re-extraction trigger** — 3 조건 명시 (fan-in ≥ 2 회복, ParsedBlock 변종 emit, medium-agnostic lift 일반화). + - 의존 그래프 ascii 갱신 (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) + ``` +- **Exit gate**: + - `sed -n '703,770p' docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "원래 의도\|현재 상태\|보존된 surface\|future re-extraction"` ≥ 4 — 4 단락 모두 존재. + - `sed -n '703,770p' docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "thin layer\|fan-in"` ≥ 1 — historical intent 의 wording 보존. + - `git diff main..HEAD -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | head -100` 의 hunk 가 line 703-764 만 touch (spec §5.6). +- **Spec 참조**: §3.5 (4-단락 wording), §5.6 (design doc 갱신 검증). + +### Step 13: design §8 graph 갱신 (line 1457-1491) — 3 edge 제거 + 2 forbidden bullet 의미 갱신 + +- **Files affected**: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` (§8 부분). +- **Action** (spec §3.6): + - **graph diff** (3 edge 제거): + ```diff + - ├─> 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 + ``` + - **commentary 갱신** (§3.7b reference 의 thin layer wording 폐기): + ```diff + -`kebab-parse-types` 는 `kebab-core` 와 parsers/normalize 사이의 thin layer (§3.7b 참조). + +`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. + ``` + - **forbidden bullet 갱신**: + ```diff + - 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 ✗ + ``` + *주의*: `kebab-parse-md → store / llm / embed ✗` 룰은 *추가 안 함* (MAJOR #5 closure — 기존 `parse-* → store/llm/embed ✗` 가 흡수된 lift 까지 자동 포함). 중복 룰 회피. +- **Exit gate**: + - `sed -n '1457,1495p' docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "kebab-parse-types"` ≤ 1 — 본문 commentary 의 historical reference 만 보존 (또는 0). + - `sed -n '1457,1495p' docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "kebab-normalize"` ≤ 1 — 동상. + - `sed -n '1457,1495p' docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "parse-\\* (pdf/image/code) → kebab-parse-md ✗"` = 1 — 신규 forbidden bullet 존재. + - `git diff main..HEAD -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` 의 hunk 가 §3.7b (Step 12) + §8 (Step 13) 의 2 section 만 touch — MINOR GAP7 closure 의 truncate-free numeric verify: + - `git diff main..HEAD -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -c "^@@"` = **2** (정확히 2 hunk). + - `git diff main..HEAD -- docs/superpowers/specs/2026-04-27-kebab-final-form-design.md | grep -cE "^\+\+\+|^---"` = **2** (file header — 1 file × 2 = 2). +- **Spec 참조**: §3.6 (§8 graph diff), MAJOR #5 closure (중복 룰 회피). + +### Step 14: ARCHITECTURE.md + tasks/INDEX.md (L169 closure + Future work 신설) + HOTFIXES.md (4-block) + HANDOFF.md (cross-link) + +- **Files affected**: + - `docs/ARCHITECTURE.md` (crate graph + directory tree). + - `tasks/INDEX.md` (L169 closure mention + Future work 섹션 신설). + - `tasks/HOTFIXES.md` (신규 entry — 4-block). + - `HANDOFF.md` (1 줄 cross-link). +- **Action**: + - **(a) `docs/ARCHITECTURE.md` 갱신** — spec §5.8: + - crate graph 의 entries 24 → 22 — `kebab-parse-types` + `kebab-normalize` 절 삭제. + - directory tree 의 `crates/kebab-parse-types/` + `crates/kebab-normalize/` 줄 삭제. + - 흡수 mention 한 줄 추가 (또는 §3.7b strike 의 cross-link). + - **(b) `tasks/INDEX.md` L169 closure** — spec §5.8: + ```diff + - - **PR #181 chore: ... system-architect 의 component-level review 결론 = pre-cut nothing, all v0.18.1+ defer (kebab-normalize 흡수, Extractor dispatch unification, kebab-source-fs dep lightening 등). + + - **PR #181 chore: ... system-architect 의 component-level review 결론 = pre-cut nothing, all v0.18.1+ defer (kebab-normalize 흡수 — v0.19.0 closure, see HOTFIXES.md 2026-05-26; Extractor dispatch unification; kebab-source-fs dep lightening 등). + ``` + - **(c) `tasks/INDEX.md` "Future work / deferred" 섹션 신설** — spec §11.7 + verified INDEX.md 에 현재 부재: + - 위치: "## Post-merge 핫픽스" 와 "## 모든 task 공통 규약" 사이 (신설). + - 본문: + ```markdown + ## Future work / deferred + + - v0.20+ image/pdf normalize integration — design §3.7b intent 미구현 (3 dead struct 보존). PR #186 (normalize-absorption) 의 spec §11 참조. + ``` + - **(d) `tasks/HOTFIXES.md` 신규 entry** — spec §3.9 의 4-block 변형 그대로 (Symptom + Root cause + Action + Amends with inline Wire/surface impact). MAJOR #4 closure — anchor 검증 generic 화: + ```bash + # insertion point 의 generic verify (hard-coded entry 인용 회피): + $ head -50 tasks/HOTFIXES.md # frontmatter + heading + first entry 위치 확인 + $ FIRST_ENTRY_LINE=$(grep -n "^## 20" tasks/HOTFIXES.md | head -1 | cut -d: -f1) + $ echo "insertion point = line ${FIRST_ENTRY_LINE} 의 *위* (chronological reverse — newest top)" + ``` + 본 시점 측정 결과 = line 17 (`## 2026-05-26 — S3 NLI unavailable`). 본 PR 의 entry 는 그 *위* 에 insert — 같은 2026-05-26 date 이지만 본 entry 는 source-fs sub-item 1 (PR #185) 의 sibling 인 sub-item 2 closure 라 chronologically later. Action 라인에 spec §11 cross-link 한 줄 포함 ("3 dead struct... 보존 — v0.20+ image/pdf normalize integration 의 future surface (spec §11 참조)"). + - **(e) `HANDOFF.md` cross-link 한 줄** — `## 머지 후 발견된 버그 / 결정` 섹션 의 가장 최근 entry 위: + ```markdown + - 2026-05-26 kebab-normalize + kebab-parse-types 흡수 (24 → 22 crates, design §3.7b 재작성). v0.19.0 cut. [HOTFIXES.md](tasks/HOTFIXES.md#2026-05-26--design-deviation--kebab-normalize--kebab-parse-types-흡수-24--22-crates). + ``` +- **Exit gate**: + - `grep -c "Future work\|Future Work" tasks/INDEX.md` ≥ 1 — 신설 섹션 존재. + - `grep -c "2026-05-26 — design deviation — kebab-normalize" tasks/HOTFIXES.md` = 1 — entry 추가. + - `grep -c "image/pdf normalize integration" tasks/INDEX.md tasks/HOTFIXES.md` ≥ 2 — 두 doc 모두 cross-link. + - `grep -c "2026-05-26 kebab-normalize" HANDOFF.md` = 1. + - `grep -c "kebab-parse-types\|kebab-normalize" docs/ARCHITECTURE.md` ≤ 1 — historical mention 만 보존 (또는 0). + - 4 frozen task spec (p1-2, p1-3, p1-4, p9-fb-07) `git diff main..HEAD` 의 path 에 0 hit. +- **Spec 참조**: §3.9 (HOTFIXES 4-block), §5.8 (ARCHITECTURE + INDEX), §11.7 (Future work 섹션 신설), §5.7 (frozen task spec invariant). + +### Step 15: workspace 회귀 + 7 cargo gate + clean commit — closure + +- **Files affected**: commit 만 (production code touch 0). +- **Action**: + - **(a) `cargo clean`** — spec §5.10 (16 GB RAM pressure 회피). + - **(b) full workspace test + numeric net-delta verify** (GAP4 closure — Step 1 의 baseline 과 numeric compare): + ```bash + $ POST_COUNT=$(cargo test --workspace --no-fail-fast -j 1 2>&1 \ + | awk '/^test result: ok\./ {for(i=1;i<=NF;i++) if($i=="passed;") sum += $(i-1)} END {print sum}') + $ echo "POST_COUNT=$POST_COUNT" + $ diff <(echo "$POST_COUNT") .omc/state/normalize-absorption-baseline.txt \ + && echo "✓ net delta = 0 (spec §5.1)" \ + || { echo "✗ net delta != 0 — REQUIRES INVESTIGATION"; exit 1; } + ``` + spec §5.1 의 expected net delta = 0 (1:1 lift, signature 무변, test 의미 무변 — Step 9 의 file 이동도 in-crate test 로 collapse 이므로 함수 수 보존). 만약 +N intentional addition 시 (예: NEW MAJOR #N1 + #N2 의 literal 보존 regression pin 신규) plan §1 approach summary 에 명시 + diff 의 numeric tolerance update. + - **(c) clippy gate** — `cargo clippy --workspace --all-targets -- -D warnings` 0 warning (spec §5.2). + - **(d) cargo deny** — `cargo deny check` 0 error 0 warning (spec §5.9). + - **(e) 22 crate workspace verify** — `cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length'` = 22 (spec §5.2 + §5.8). + - **(f) Cargo.lock 검증** — spec §5.11: + ```bash + $ grep '^name = "kebab-normalize"\|^name = "kebab-parse-types"' Cargo.lock + (0 hit) + $ awk '/^\[\[package\]\]/,/^$/{if(/name = "kebab-parse-md"/)f=1; if(f) print; if(/^$/ && f){f=0; print "---"}}' Cargo.lock | grep "unicode-normalization" + unicode-normalization + ``` + - **(g) dep tree invariant** — `cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)"` = 0 줄. + - **(h) wire schema diff = 0** — `git diff main..HEAD -- docs/wire-schema/v1/ | wc -l` = 0 (spec §5.4). + - **(i) clean commit**: + ```bash + git add -A + git status # verify + git commit -m "$(cat <<'EOF' + 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 green / cargo clippy --workspace + --all-targets -- -D warnings 0 warning / cargo deny check 0 error / cargo metadata ... + workspace_members | length = 22 / cargo tree -p kebab-app | grep kebab_parse_types + + kebab_normalize = 0 줄. + EOF + )" + ``` +- **Exit gate**: + - 모든 cargo gate green (a-h). + - `git log --oneline -1` = 위 commit message 의 first line. + - `git status` = clean (untracked file 0). +- **Spec 참조**: §5.1-§5.11 (모든 verification gate). + +## §3 Step dependency graph + +```text +Step 1 (Pre-flight baseline) + │ + ▼ +Step 2 (types.rs 신설) ──┐ + │ │ parallel OK (둘 다 in-crate, lib.rs mod declare 아직 없음) + ▼ │ +Step 3 (normalize.rs 신설 + literal 보존) ◄─┘ + │ + ▼ +Step 4 (lib.rs mod + pub explicit re-export) + │ + ▼ +Step 5 (blocks.rs + frontmatter.rs use shift) + │ + ▼ +Step 6 (kebab-app callsite lib.rs:51 + :1119) + │ + ▼ +Step 7 (kebab-app Cargo.toml dep cleanup) — anchor 1 + │ + ▼ +Step 8 (kebab-chunk + kebab-store-sqlite dev-dep migration) + │ + ▼ +Step 9 (test file 이동 + 자기 참조 verify) + │ + ▼ +Step 10 (workspace Cargo.toml Hunk a + b) — anchor 2 + │ + ▼ +Step 11 (kebab-normalize/ + kebab-parse-types/ 디렉토리 삭제) + │ + ▼ +Step 12 (design §3.7b 4-단락 재작성) + │ + ▼ +Step 13 (design §8 graph 3 edge 제거 + 2 forbidden bullet 갱신) + │ + ▼ +Step 14 (ARCHITECTURE + INDEX + HOTFIXES + HANDOFF) + │ + ▼ +Step 15 (회귀 + 7 cargo gate + clean commit) — closure +``` + +핵심 invariant: + +- **Step 2 + 3 < Step 4**: file 생성 후 lib.rs mod declare — 동시 commit 시 cargo 자동 link. +- **Step 4-5 < Step 6**: 동일 crate 내 ref shift 후 외부 caller redirect. +- **Step 6 < Step 7**: callsite migration 후 dep 제거 — kebab-app build green 보장. +- **Step 7 < Step 8**: kebab-app 정합 후 dev-dep migration (sequential gate 분리, 자체 dep 영향 0). +- **Step 8 < Step 9**: dev-dep cleanup 후 test file 이동. +- **Step 6-9 < Step 10**: anchor 1 (kebab-app) + dev-dep + test file 모두 정합 후 anchor 2 (workspace.members). +- **Step 10 < Step 11**: workspace.members 제거 후 디렉토리 삭제 — stale path 회피. +- **Step 11 < Step 12-14**: production code 갱신 후 design + doc 갱신 — reality ≡ contract. +- **Step 12 < Step 13**: §3.7b 재작성 후 §8 graph 갱신 (§8 commentary 가 §3.7b 인용). +- **Step 13 < Step 14**: design 갱신 후 ARCHITECTURE + INDEX + HOTFIXES + HANDOFF 갱신 — design 이 source-of-truth, doc 들이 그 mirror. +- **Step 14 < Step 15**: 모든 file change 후 commit. + +## §4 Verification gate (acceptance) + +### §4.1 Step 별 verify (per-step exit gate) + +각 Step 의 "Exit gate" 항목이 step-local gate. 핵심 anchor: + +- **Step 4** (lib.rs re-export): destination surface alive — `cargo build -p kebab-parse-md` green. +- **Step 7** (kebab-app dep cleanup): anchor 1 — `cargo tree -p kebab-app | grep ...` = 0 줄. +- **Step 10** (workspace.members): anchor 2 — `cargo metadata ... | jq '.workspace_members | length'` = 22. +- **Step 11** (디렉토리 삭제): final invariant — `ls -d crates/*/ | wc -l` = 22 + `cargo build --workspace` green. +- **Step 15** (closure): 7 cargo gate (a-h) + wire diff 0 + clean commit. + +### §4.2 Workspace 회귀 (spec §5.1, §5.2) — Step 15 시점 + +```bash +$ cd /home/altair823/kebab && export CARGO_TARGET_DIR=/build/out/cargo-target/target +$ cargo clean +$ cargo test --workspace --no-fail-fast -j 1 # baseline ± 작은 변동 (net delta = 0 또는 +N intentional) +$ cargo clippy --workspace --all-targets -- -D warnings # 0 warning +$ cargo deny check # 0 error 0 warning +$ cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length' # = 22 +$ ls -d crates/*/ | wc -l # = 22 (secondary) +$ cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)" # 0 line +$ grep -rn "kebab_normalize\|kebab_parse_types" crates/kebab-app/src/ # 0 hit +$ grep -l "kebab-normalize\|kebab-parse-types" crates/*/Cargo.toml # 0 line +$ grep '^name = "kebab-normalize"\|^name = "kebab-parse-types"' Cargo.lock # 0 hit +``` + +### §4.3 Wire schema 회귀 (spec §5.4) + +```bash +$ git diff main..HEAD -- docs/wire-schema/v1/ | wc -l # = 0 +``` + +### §4.4 4 frozen task spec + ~25 referencing task spec frozen (spec §5.7) + +```bash +$ git diff main..HEAD --name-only | grep -E "tasks/p1/p1-(2|3|4)|tasks/p9/p9-fb-07" # 0 line +$ git diff main..HEAD --name-only | grep "^tasks/p" | wc -l # 0 (모든 task spec mechanical update 0) +``` + +### §4.5 SMOKE 회귀 (spec §5.5) — informational only + +`docs/SMOKE.md` 가 정의한 isolated TempDir KB pipeline 의 ingest + search + ask 가 흡수 전후 byte-identical wire 출력. *informational only* — acceptance gate 아님 (production code 의 lift 로직 byte-identical 이므로 SMOKE 결과 자동 일관). + +### §4.6 Literal 보존 verify (spec §3.7 (e)(g) + NEW MAJOR #N1 + #N2) + +Step 3 + Step 15 에서 두 번 verify: + +```bash +$ grep -c 'target: "kebab-normalize"' crates/kebab-parse-md/src/normalize.rs # = 1 (tracing target literal) +$ grep -E '"kb-(source-fs|parse-md|normalize)"' crates/kebab-parse-md/src/normalize.rs | wc -l # >= 5 (4 hard-coded agent literal + warning_agent body return) +``` + +## §5 Commit strategy + +**single clean commit** — sibling plan (PR #185) 의 패턴. 본 PR 의 모든 change 가 atomic refactor 이므로 split 의 의미 없음. + +- Step 1-14 의 file edit 을 모두 staging 후 Step 15 의 commit message 로 single commit. +- 중간 step 의 *작업 progress* 는 git stash 없이 working tree 에 누적 (cargo build/test green 유지 시 — exit gate 가 step 별 정합 보장). +- Step 10-11 사이에 cargo build *fail* 또는 *workspace 무시* 발생 시 — expected behavior (§2 의 Step 10 exit gate 명시). +- 만약 Step 11 의 디렉토리 삭제 후 unexpected build error 발생 시 — `git reset --hard HEAD` 으로 모든 working change drop 하고 plan 재진입 (sibling plan §6.1 와 동일 패턴). + +Commit message 의 구조 (Step 15 의 (i) 참조): +- first line: `refactor(parse-md): absorb kebab-normalize + kebab-parse-types — 24 → 22 crates + §3.7b 재작성` +- 본문: 14 bullet (deliverable + wire/surface impact + verification) + Co-Authored-By 제거 (사용자 commit pattern 와 일관). + +## §6 Risks + mitigation + +### §6.1 중간 단계 cargo build 깨짐 (step ordering 깨짐) + +**위험**: Step 6 (kebab-app callsite) 가 Step 2-4 (destination 생성) 보다 먼저 진행되면 `use kebab_parse_md::build_canonical_document;` 의 destination surface 가 alive 안 됨 → cargo build fail. + +**Mitigation**: §3 step dependency graph 의 ordering invariant 명시. Step 별 exit gate 가 cargo build green 보장 — gate 통과 후 다음 step. + +### §6.2 Step 10 anchor 후 build fail (workspace.members 미정합) + +**위험**: Step 10 의 Hunk (a) 만 적용하고 Step 11 디렉토리 삭제 안 하면 cargo 가 `crates/kebab-normalize/Cargo.toml` 의 path 를 lingering 으로 인식 가능 (단 members 에 없으므로 silently skip 예상). + +**Mitigation**: Step 10 의 exit gate 가 "build *fail* 예상 또는 *workspace 무시*" 명시 — *expected* behavior. Step 11 즉시 진행으로 정합. + +### §6.3 자기 참조 dev-dep 의 cargo behavior 미검증 + +**위험**: Step 9 의 `crates/kebab-parse-md/tests/normalize_snapshot.rs` 가 *자기 자신* 의 `lib` 를 link 하는 패턴. cargo 의 standard behavior 는 dev-dep declare 없이 integration test 가 자기 crate `lib` 자동 link — 그러나 misconfig 시 silently no-link 위험. + +**Mitigation**: spec §6.3 R3 + §3.7 (f) 의 명시 — `cargo test -p kebab-parse-md --test normalize_snapshot -j 1` 가 green 인지 Step 9 의 exit gate 에서 확인. 만약 fail 시 *명시적* `kebab-parse-md = { path = "." }` dev-dep declare 추가 (cargo 의 어떤 edge case 일 수 있음). + +### §6.4 hard-coded literal accidental drop + +**위험**: Step 3 의 normalize.rs 이식 시 4 hard-coded agent literal (lib.rs:122/128/134/153) + tracing target literal (lib.rs:109) 중 일부가 cp/Write 과정에서 누락. spec §3.7e + §3.7 (g) + §1.9 의 6-row production flow trace 정합 깨짐. + +**Mitigation**: Step 3 의 exit gate 가 `grep -c 'target: "kebab-normalize"' ... = 1` + `grep -E '"kb-(source-fs|parse-md|normalize)"' ... | wc -l >= 5` 명시. 5 literal (4 agent + 1 tracing target) 모두 grep 으로 verifiable. §4.6 의 spec-level verify gate 도 cross-check. + +### §6.5 ~25 referencing task spec 의 accidental edit + +**위험**: Step 12-14 의 design + doc 갱신 시 ~25 referencing task spec 도 같이 검색-치환 하면 frozen 룰 위반. + +**Mitigation**: spec §5.7 의 invariant — `git diff main..HEAD --name-only | grep "^tasks/p"` = 0 line. Step 14 종료 후 verify 로 확인. 만약 stray edit 발생 시 `git checkout main -- tasks/p/...` 으로 revert. + +### §6.6 16 GB RAM 의 build pressure (full workspace test) + +**위험**: Step 15 의 `cargo test --workspace --no-fail-fast -j 1` 가 lance / datafusion link step 에서 OOM 위험 (MEMORY.md `feedback_serial_build_only.md` + CLAUDE.md "Serial cargo builds only"). + +**Mitigation**: +- `cargo clean` 직전 후 (Step 15 (a)) — stale artifact 제거로 link memory 절약. +- `-j 1` 엄수 (sibling plan §6.6 패턴). +- per-crate 단위 검증 우선 (Step 2-9 의 각 exit gate 가 `-p ` 단위) — full workspace 는 Step 15 의 1회. +- `cargo test/clippy/build` 동시 background 실행 금지. + +### §6.7 design §3.7b 재작성 wording 의 부정확 + +**위험**: Step 12 의 §3.7b 재작성 시 spec §3.5 의 4-단락 wording 을 정확히 따르지 않으면 critic round 5 (만약 spec 이 다회 revision) 의 verification 가 fail 가능. + +**Mitigation**: Step 12 의 source-of-truth = spec §3.5 의 code block (한국어 산문 + 의존 그래프 ascii). plan/executor 가 spec §3.5 를 copy-paste 후 markdown formatting (heading depth 등) 조정. + +### §6.8 HOTFIXES.md entry 의 chronological reverse insert 실수 + +**위험**: Step 14 의 HOTFIXES entry 신규 추가 시 chronological reverse (newest top) 룰 위반 — 본 PR 의 entry 가 기존 entry *밑* 에 들어가면 reader 혼란. + +**Mitigation**: Step 14 (d) 의 generic anchor verify cmd 활용 — `head -50 tasks/HOTFIXES.md` + `FIRST_ENTRY_LINE=$(grep -n "^## 20" tasks/HOTFIXES.md | head -1 | cut -d: -f1)` 으로 first entry line 위치 동적 측정 후 그 *위* 에 insert. hard-coded date / wording 인용 회피 — file 의 history 변경에도 robust. + +### §6.9 kebab-parse-md 의 dep 폭증 (spec §6.9 R9) + +**위험**: 흡수 후 `kebab-parse-md/Cargo.toml` 의 deps 가 기존 + `unicode-normalization` (흡수 후 추가) 로 1개 증가. lingua 의 build time + binary size 가 markdown parse + lift 두 책임을 모두 가지는 crate 에 concentrate. + +**Mitigation**: +- 신규 deps = `unicode-normalization` 1 개만 (이미 `kebab-app` 도 사용 중인 `0.1` major). version drift 없음. +- Step 3 의 normalize.rs 이식 시 `Cargo.toml` 의 `[dependencies]` 에 `unicode-normalization = "0.1"` 추가 (sibling normalize 의 dep 와 동일 version). +- 본 위험의 실질 영향 ≈ +1 dep → 영향 minimal. + +### §6.10 frozen p1-4 surface re-export 후퇴 (spec §6.10 R10) + +**위험**: Step 3 의 normalize.rs 이식 시 `pub use kebab_core::{id_for_block, id_for_doc}` 제거 → p1-4 frozen public surface 의 후퇴. + +**Mitigation**: +- spec §6.10 R10 의 production caller 0 verified — re-export 경유 caller = 0 (test mod imports 제외, R10 의 grep cmd). +- Step 3 의 exit gate 가 `grep -c "pub use kebab_core::" crates/kebab-parse-md/src/normalize.rs` = 0 verify — 명시적 제거. +- p1-4 frozen 룰은 historical contract 로 보존 — 본 PR 의 HOTFIXES entry (Step 14) 가 live source. + +## §7 Out of scope (plan-level) + +spec §8 의 모든 out-of-scope 그대로 + plan-level 추가: + +- sibling spec (PR #185 source-fs dep lightening) 의 follow-up. 별 PR / 별 plan. +- kebab-parse-md 의 internal refactor (예: types.rs + normalize.rs 외 module 재배치). follow-up. +- kebab-app/src/lib.rs 의 다른 callsite 의 cosmetic 변경 — line 51, 1119 만 touch. +- Lens 3 (Extractor + Chunker dispatch unification) — 별도 작업 (spec §8 + §11 의 future direction sibling). +- v0.20+ image/pdf normalize integration — spec §11 의 영구 보존 entry (본 PR 의 scope 외). +- **kebab-app/src/lib.rs 의 comment 안 historical `kb-normalize` mention** (verified — line 1308 의 `mirroring \`kb-normalize::build_canonical_document\`` + line 1474-1475 의 `see kb-normalize's \`warning_agent\``) — comment 의 historical reference 로 **보존** (MINOR GAP8 closure). git blame 일관성 + reader 가 흡수 history 의 origin 추적 가능. context string (line 1119) 만 갱신 (Step 6) — production runtime behavior 의 wire-invisible 영향과 무관한 internal doc 형식의 historical reference 보존. + +## §8 References + +- spec: `docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md` (1067 lines, round 3 APPROVE). +- sibling plan: `docs/superpowers/plans/2026-05-26-source-fs-dep-lightening-plan.md` (PR #185 머지 완료). +- design contract: `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §3.7b + §8. +- 4 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`. +- audit log root: `tasks/INDEX.md` L169 (PR #181 의 system-architect review). +- CLAUDE.md (project) + CLAUDE.md (machine) + MEMORY.md — disk / cargo / commit 룰. + +## §9 Round closure status + +### §9.1 Round 1 status + +| Round | Reviewer | Verdict | Issues | Closure | +|---|---|---|---|---| +| 1 | critic-plan + verifier-plan (round 1) | REQUEST_CHANGES (2 CRITICAL + 2 BLOCKER + 6 MAJOR + 5 MINOR + 1 NIT = 16) | 본 round 2 revision 에서 all-closed — §9.2 의 finding-by-finding closure 참조 | 본 plan 의 round 2 revision (2026-05-26, planner) | + +### §9.2 round 1 finding-by-finding closure + +| ID | Severity | Source | Finding | Closure (plan-level edit location) | +|---|---|---|---|---| +| #1 (critic) | CRITICAL | critic-plan | `kebab-parse-md/Cargo.toml` 의 dep 갱신 step 부재 (spec §3.4 의 `-kebab-parse-types` + `+unicode-normalization` silently drop) | **CLOSED** — Step 3 의 Files affected 에 `crates/kebab-parse-md/Cargo.toml` 추가 + Action (e) 신설 (description string 갱신 + `kebab-parse-types` 제거 + `unicode-normalization = "0.1"` 추가). Exit gate 의 `grep -c "kebab-parse-types" Cargo.toml = 0` + `grep -c "unicode-normalization" Cargo.toml ≥ 1`. | +| #2 (critic) | CRITICAL | critic-plan | `pub use kebab_core::{id_for_block, id_for_doc}` 제거 시 in-body unqualified call (line 67, 241) unresolved → compile error | **CLOSED** — Step 3 의 Action (d) 갱신: `pub use` 제거 + 기존 `use kebab_core::{...}` block 에 `id_for_block, id_for_doc` 추가 명시. Exit gate `grep -E "use kebab_core::\{" ... \| grep -c "id_for_block\|id_for_doc" ≥ 1`. (대안 b: spec §3.3 retract — 채택 안 함, plan-side fix only) | +| #1 (verifier) | BLOCKER | verifier-plan | `kebab-parse-md/tests/` 의 `kebab_parse_types` import (2 hit verified — blocks_snapshots.rs:19 + frontmatter_snapshots.rs:23) 갱신 step 부재 | **CLOSED** — Step 5 의 Files affected 에 2 test file 추가 + Action (c)(d) 신설 (`kebab_parse_md::*` 로 갈음, integration test 는 `crate::` 사용 불가). Exit gate 의 grep scope src/ 한정 해제 → `grep -rn "kebab_parse_types" crates/kebab-parse-md/` = 0 hit. | +| #5 (verifier GAP5) | MAJOR | verifier-plan | blocks.rs 의 4 hit (line 1 doc, 25 doc-link, 37 use, 1589 production fully-qualified) 갱신 | **CLOSED** — Step 5 의 Action (a) 의 file-wide replace 명시. 4 hit 모두 `kebab_parse_types::` → `crate::types::` (Edit `replace_all: true` 가능). | +| (extra finding) | (planner self-detect) | normalize.rs | `cfg(test) mod tests` 안 9 hit fully-qualified `kebab_parse_types::ParsedBlockKind::*` (line 489/498/507/516/525/818/862/1070) 갱신 필요 | **CLOSED** — Step 3 의 Action (c) 의 sed/replace_all 명시. Exit gate `grep -c "kebab_parse_types::" crates/kebab-parse-md/src/normalize.rs = 0`. | +| #1 (critic) | MAJOR | critic-plan | Step 7 verify gate self-contradictory ("build fail 예상" — actual cargo behavior = silently ignore) | **CLOSED** — Step 10 verify gate 의 wording 정정: "`cargo build --workspace -j 1` **green** — 두 orphan 디렉토리는 cargo silently ignore. Cargo.lock 의 `[[package]]` entry 는 Step 11 직후 first `cargo build` 에서 자동 cleanup." (sibling plan revision 1 의 Step 7 → revision 2 의 Step 10) | +| #2 (critic) | MAJOR | critic-plan | Step 5 의 generic glob → 2 file:line 명시 | **CLOSED** — Step 8 의 Action (c) 갱신: `chunk/tests/long_section_snapshot.rs:21` + `store-sqlite/tests/contract_roundtrip.rs:16` explicit. | +| #3 (critic) | MAJOR | critic-plan | Step 6 의 fixture file enumerate (find 결과 명시) | **CLOSED** — Step 9 의 Files affected 갱신: `find crates/kebab-normalize/tests/` 결과 = `normalize_snapshot.rs` 단일 file (sibling fixture 부재) 명시. | +| #4 (critic) | MAJOR | critic-plan | HOTFIXES insertion anchor stale (hard-coded "S3 NLI" entry 인용) → generic 화 | **CLOSED** — Step 14 의 Action (d) 갱신: `head -50 tasks/HOTFIXES.md` + `FIRST_ENTRY_LINE=$(grep -n "^## 20" tasks/HOTFIXES.md \| head -1)` 동적 측정. §6.8 risk wording 도 generic 화. | +| GAP3 + GAP4 (verifier) | MAJOR | verifier-plan | Step 1 의 baseline N + Step 10 의 net delta 0 numeric compare cmd 부정확 (binary 수 행수 ≠ test 함수 수) | **CLOSED** — Step 1 의 baseline cmd 갱신: `awk '/^test result: ok\./ {sum += "passed;" 직전 숫자} END {print sum}'`. `.omc/state/normalize-absorption-baseline.txt` dump. Step 15 의 (b) numeric compare gate 추가: `diff <(echo $POST_COUNT) .omc/state/normalize-absorption-baseline.txt`. | +| GAP6 | MINOR | verifier-plan | Step 2 (현 Step 3) 의 4-literal grep production-only 분리 | **CLOSED** — Step 3 의 exit gate 의 grep cmd 정확화: `grep -cE 'agent:\s*"kb-(source-fs\|parse-md\|normalize)"\.to_string\(\)' ≥ 3` (production body 의 hard-coded literal 만). warning_agent body return + lift_warnings literal 합쳐 5 hit 보장. | +| GAP7 | MINOR | verifier-plan | Step 9 verify `git diff ... \| head -100` 의 truncate 위험 | **CLOSED** — Step 13 의 exit gate 갱신: `head -200` 삭제 + `grep -c "^@@" = 2` + `grep -cE "^\+\+\+\|^---" = 2` numeric verify 추가. | +| GAP8 | MINOR | verifier-plan | lib.rs comment stale `kb-normalize` mention (line 1308, 1474) 보존 결정 명시 | **CLOSED** — §7 out-of-scope 에 한 줄 추가: "`kebab-app/src/lib.rs` 의 comment 안 historical `kb-normalize` mention 은 보존 — git blame 일관성 + reader 가 흡수 history 추적 가능. context string (line 1119) 만 갱신." | +| #3 (critic) | MINOR | critic-plan | Step 4 (c) Cargo.toml hunk context — `sed -n '11,20p'` actual context 확인 명시 | **CLOSED** — Step 7 의 Action 갱신: `sed -n '11,20p' crates/kebab-app/Cargo.toml` 사전 context 확인 cmd 추가. | +| #1 (critic) | NIT | critic-plan | Step 7 "anchor" 명명 의미 — "closure pre-pivot" 정도 | **CLOSED** — Step 7 의 heading 정정: "anchor" → "closure pre-pivot 1". Step 10 의 "anchor 2" 도 의미상 "closure pre-pivot 2" 로 변경 가능하나 plan 의 convention 일관성 유지 차원에서 "anchor 1" / "anchor 2" / "closure" 의 3-stage 명명 유지 (NIT 수준 — semantic only). | + +### §9.3 Round 2 metrics + +- Plan line count: 567 (revision 1) → 761 (revision 2 start) → **현재** (revision 2 end, post-16-finding-closure). +- Step count: 15 (revision 2 무변). +- 신규 추가 (revision 2 end): + - Step 3 의 Action (e) — kebab-parse-md/Cargo.toml diff. + - Step 3 의 Action (d) — id_for_* unqualified import 보존. + - Step 3 의 Action (c) — 9 hit fully-qualified 갱신. + - Step 5 의 Action (c)(d) — kebab-parse-md/tests/ 의 2 file ref shift. + - Step 1 + Step 15 의 baseline N + numeric compare. + - §7 의 lib.rs comment 보존 명시. + - §9.2 의 16-row closure table. +- §3 Design 결정 무변 (Option A, dead struct 3 보존, §3.7b 4-단락 재작성, target_version 0.19.0, warning_agent + tracing target 보존 정책, id_for_* re-export 제거). + +--- + +**Plan drafted by**: planner (team `normalize-absorption`, Phase B). +**Date**: 2026-05-26. +**Source spec**: 2026-05-26-normalize-absorption-spec.md (round 3 APPROVE, 1067 lines). +**Step count**: 15 (decompose from 10 step revision 1). +**Step ordering invariant**: Step 2 + 3 < Step 4-5 < Step 6 < Step 7 < Step 8 < Step 9 < Step 10 < Step 11 < Step 12 < Step 13 < Step 14 < Step 15. +**Anchor steps**: Step 7 (kebab-app dep cleanup) + Step 10 (workspace.members) + Step 15 (closure). +**Estimated complexity**: MEDIUM (15 step, 1:1 spec mapping, 단일 commit, ~3000 LOC code touch). diff --git a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md index a479209..4302716 100644 --- a/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md +++ b/docs/superpowers/specs/2026-04-27-kebab-final-form-design.md @@ -700,11 +700,9 @@ pub enum AudioType { M4a, Mp3, Wav, Flac, Ogg, Other(String) } `OffsetDateTime` 는 `time::OffsetDateTime`, `Result` 는 crate-local alias. -### 3.7b Parser intermediate types — `kebab-parse-types` +### 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 를 책임지고 어떤 parser 도 직접 import 하면 안 된다. 그러나 normalize 에 들어오는 입력 타입이 어딘가에 정의되어야 하는데, 그것을 `kebab-core` 에 박으면 (a) parser-별 ParsedBlock 변종 (`ParsedImageRegion`, `ParsedPdfPage`, `ParsedAudioSegment`) 이 향후 합류할 때 core 의 namespace 가 폭발하고, (b) parser 의 의미 변경이 core 변경이 되어 모든 의존자가 영향을 받는다. - -`kebab-parse-types` 는 이 둘 사이의 **유일한 layer** 다. 의존 그래프: +**원래 의도**: parser 의 *중간* 표현 (`ParsedBlock` 류) 을 `kebab-core` 가 아닌 별도 thin crate `kebab-parse-types` 에 두고, `kebab-normalize` 가 medium-agnostic 한 ID/Provenance lift 책임을 가지는 layered 구조 (v0.1~v0.18 머지 시점의 초기 design). 의도된 의존 그래프: ```text kebab-core (도메인 모델 — Block, Chunk, SourceSpan, IDs, …) @@ -717,16 +715,18 @@ kebab-parse-md, kebab-parse-pdf, kebab-normalize kebab-parse-image, kebab-parse-audio ``` -`kebab-parse-types` 는: -- `kebab-core` 에만 의존 (`Block`, `SourceSpan`, `Lang` 등 도메인 타입 사용). -- 다른 어떤 `kebab-*` 에도 의존하지 않는다. -- 어떤 parser 의 구체 라이브러리 (`pulldown-cmark`, `pdf-extract`, `image`, `whisper-rs`) 에도 의존하지 않는다. -- serde + thiserror 정도의 외부 의존만 가진다. +이 thin layer 의 raison d'être 는 (a) parser-별 ParsedBlock 변종이 `kebab-core` 의 namespace 를 폭발시키지 않게 분리하고, (b) `kebab-normalize` 가 어떤 parser 도 직접 import 하지 않는 medium-agnostic lift 단계를 유지하는 것이었다. -P1 에서 정의되는 타입: +**현재 상태 (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. +- `kebab-normalize` 의 production caller 가 1개 (`kebab-app`) 로 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. 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. ```rust -// kebab-parse-types — depends on kebab-core only. +// kebab-parse-md::types — in-crate module (v0.19.0 흡수 후). depends on kebab-core only. pub struct ParsedBlock { pub kind: ParsedBlockKind, pub heading_path: Vec, @@ -749,19 +749,39 @@ pub enum ParsedPayload { pub struct Warning { pub kind: WarningKind, pub note: String } pub enum WarningKind { MalformedFrontmatter, MalformedTable, EncodingFallback, ExtractFailed } -``` -`Inline` 은 `kebab-core` (§3.4) 에 있는 도메인 타입. `kebab-parse-types` 는 그것을 *참조* 만 한다 — 같은 의미를 두 crate 에 중복 정의하지 않는다 (그러면 normalize 가 identity-conversion 을 해야 해서 무의미). - -P6/P7/P8 에서 추가될 타입 (forward-ref): - -```rust -pub struct ParsedImageRegion { /* OCR/EXIF 추출 전 단계 */ } +// Forward-declared (P6/P7/P8) — production caller 0, future re-extraction trigger surface 로 보존. +pub struct ParsedImageRegion; pub struct ParsedPdfPage { pub page: u32, pub text: String } pub struct ParsedAudioSegment { pub start_ms: u64, pub end_ms: u64, pub text: String } ``` -→ 새 medium 추가 시 `kebab-core::Block` 변종은 변하지 않고, `kebab-parse-types` 만 확장된다. +**future re-extraction trigger** (측정 시점 명시 — `build_canonical_document` 의 input variant 변경 지점): + +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` 외 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`) 에 의존. + +**Tracing instrumentation policy**: actual `kebab-parse-md/src/normalize.rs` 의 `tracing::debug!` 가 **explicit literal** `target: "kebab-normalize"` 로 hard-coded (자동 module-path derive 아님). 흡수 후에도 보존 — stage label 보존 정책 (warning_agent 보존 + `provenance.events[].agent` 보존과 일관) 시 stage label = "kebab-normalize" — 흡수 후에도 lift stage 의 의미 보존 + log scraper grep 일관성. 명시적 갱신 원할 시 `target: "kebab-parse-md::normalize"` — 본 design 의 권장 = **보존**. wire / surface impact 0. ### 3.8 Answer / RAG types @@ -1459,12 +1479,11 @@ 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) @@ -1477,15 +1496,13 @@ kebab-cli, kebab-tui, kebab-desktop └─> 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. 기존 `parse-* → store/llm/embed ✗` 룰이 흡수된 lift 까지 자동 포함 — parse-md 도 parse-* 의 한 갈래. 핵심 금지: - 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 ✗ `cargo deny` + workspace deny.toml + CI 체크로 강제. diff --git a/docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md b/docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md new file mode 100644 index 0000000..cd538fa --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md @@ -0,0 +1,1068 @@ +--- +status: drafting +target_version: 0.19.0 +spec: docs/superpowers/specs/2026-04-27-kebab-final-form-design.md (§3.7b 재작성 + §8 graph 갱신) +contract_sections: ["§3.7b", "§8"] +related_specs: + - docs/superpowers/specs/2026-04-27-kebab-final-form-design.md + - docs/superpowers/specs/2026-05-26-source-fs-dep-lightening-spec.md +related_plans: [] +hotfix_links: [] +--- + +# 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` 흡수 시: + +1. `kebab-app` 의 1 줄 use statement + call site (lib.rs:51, :1119) 갱신. +2. `kebab-chunk` + `kebab-store-sqlite` 의 dev-dep `kebab-normalize` → `kebab-parse-md` 로 갈음 + 두 crate 의 통합 test source (`tests/*.rs`) 의 `use kebab_normalize::*;` → `use kebab_parse_md::*;` 갱신. +3. `kebab-normalize/tests/normalize_snapshot.rs` 의 `kebab-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` (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): + +```rust +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, + parser_version: &ParserVersion, + warnings: Vec, +) -> Result; + +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` 을 받아 `CanonicalDocument` 로 lift. ID 생성 / Provenance event 누적 / Warning → ProvenanceEvent 변환 / `warning_agent` 분기 (md vs lift-stage attribution). `metadata` 는 by-value — 내부에서 `user` map 의 `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` 가 손에 있어야 함. 본 의미가 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: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_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` 타입을 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-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`** (변경): +```diff + [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`** (변경): +```diff + [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`** (변경): +```diff + [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`** (변경): +```diff + [dev-dependencies] + tempfile = "3" + serde_json = { workspace = true } + # kb-parse-md / kb-normalize / kb-chunk are dev-only — used to build a + # CanonicalDocument + Vec 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 시 사용): +```bash +# 흡수 전 (현 시점) — 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 (제안)**: + +```markdown +### 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` 외 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): + +```diff +@@ 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`**: +```diff +-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)**: +```diff +-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:134` — `Normalized` event 의 `agent` field (build_canonical_document body 의 lift 종료 시점 ProvenanceEvent push). + - `lib.rs:153` — `lift_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). + +```rust +// 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`): + +```rust +// 본 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 이동**: +```diff +-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 삭제: + +```diff + [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 변경: + +```diff + [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 변형을 추가. + +```markdown +## 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` → `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 동작 확인 필요. + +- **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 + +흡수 전: +```bash +$ 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: +```bash +$ 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`: +```bash +$ 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 + +```bash +# 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). + +```bash +$ cargo clean +$ cargo test --workspace --no-fail-fast -j 1 # full re-build + test +``` + +### §5.11 `Cargo.lock` 변경 검증 + +```bash +# 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 룰 — "Run `cargo clean` routinely 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-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 검증)**: + +```bash +# 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_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 중복 | **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 가 동일 `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 integration** — `ImageExtractor::extract` 가 `Vec` 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 integration** — `PdfTextExtractor::extract` 가 `Vec` 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: + ```markdown + ## 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). diff --git a/tasks/HOTFIXES.md b/tasks/HOTFIXES.md index ec96cf5..947aaf6 100644 --- a/tasks/HOTFIXES.md +++ b/tasks/HOTFIXES.md @@ -14,6 +14,31 @@ historical contract that was implemented; this file accumulates the deltas so phase 5+ readers can find the live behavior without diffing git history. +## 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` → `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 spec §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` (line 55 의 기존 use list 와 통합). line 1119 context string `kb-normalize::build_canonical_document` → `kb-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). +- `crates/kebab-chunk/Cargo.toml` + `crates/kebab-store-sqlite/Cargo.toml`: `[dev-dependencies] kebab-normalize` 제거. 통합 test source (`tests/long_section_snapshot.rs:21` + `tests/contract_roundtrip.rs:16`) 의 `use kebab_normalize::build_canonical_document` → `kebab-parse-md` use list 통합. +- `crates/kebab-normalize/tests/normalize_snapshot.rs` → `crates/kebab-parse-md/tests/normalize_snapshot.rs` (mechanical move + use shift). +- `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`). +- `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §3.7b 재작성 (보존 + future re-extraction trigger 명시) + §8 graph 갱신 (3 edge 제거 + 2 forbidden bullet 의미 갱신). +- `docs/ARCHITECTURE.md` crate graph + 디렉토리 tree mechanical 갱신. +- `tasks/INDEX.md` L169 의 "kebab-normalize 흡수" defer mention 해소 + "Future work / deferred" 섹션 신설 (image/pdf normalize integration entry). + +**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 일관성.) + ## 2026-05-26 — S3 NLI unavailable — hypothesis truncate + token-count fallback **Symptom**: S3 dogfood query (`"Why does kebab combine multilingual-e5, LanceDB, and RRF together?"`) 가 NLI 활성 (`rag.nli_threshold > 0`) 시 `nli_model_unavailable` 일관 fail. `~/.local/state/kebab/logs/kb.log.2026-05-26` 의 5 회 WARN 라인 `tokenizer.encode failed: Truncation error: Sequence to truncate too short to respect the provided max_length`. diff --git a/tasks/INDEX.md b/tasks/INDEX.md index b98ef46..77fe348 100644 --- a/tasks/INDEX.md +++ b/tasks/INDEX.md @@ -166,12 +166,16 @@ P0~P5 는 직렬. P6~P9 는 P5 이후 병렬 가능. - **PR #178 PR-9c-1 core types + wire scaffolding** — ✅ 머지 (2026-05-26). `RefusalReason::NliVerificationFailed` + `NliModelUnavailable` (serde rename_all snake_case, wire = identical strings). `Answer.verification: Option` additive minor wire. `NliCfg` + `RagCfg.nli_threshold` (default 0.0) + env override. `RagPipeline.verifier` field + `with_verifier` builder. wire schemas + `docs/ARCHITECTURE.md` Mermaid 갱신. - **PR #179 PR-9c-2 pipeline integration + mock test + SKILL.md** — ✅ 머지 (2026-05-26). ★ 첫 user-visible behavior. `ask_multi_hop` step 8.5 NLI hook (empty answer 가드 + `truncate_for_nli` + verifier.score + verification field + refusal 분기) + `App::open_with_config` 의 NliVerifier construction + 5 mock multi-hop tests + SKILL.md NLI 안내 한 단락. - **PR #180 PR-9d dogfood retest + HOTFIXES closure + corpus 보존** — ✅ 머지 (2026-05-26). 동일 dogfood corpus 의 S7/S1/S3/S10 multi-hop retest — S7 PR-8 baseline `grounded=true + Adam hallucination` → PR-9 `nli_verification_failed, nli_score 0.0035` (HALLUCINATION FIXED 확정). `docs/dogfood/v0.18.0/` 신규 — sanitized SUMMARY + 4 sample wire JSON 보존. - - **PR #181 chore: workspace-wide cleanup + post-PR9 refactor** — ✅ 머지 (2026-05-26). v0.18.0 cut 전 마지막 정리. `[workspace.lints.clippy] pedantic = warn` + 의도적 30+ allow (각 rationale inline). 128 files mechanical clippy --fix. OMC team `post-pr9-refactor` 가 추가 H1 (`[models.nli].model` config wiring — `DEFAULT_MODEL_ID` 제거 + provider 분기) + H2 (`truncate_for_nli` stub `_hypothesis` 제거) + H3 (`was_truncated` tracing::debug! surface) + D (MCP test flake fix) + E (HOTFIXES cross-link) + 9 new tests (T1-T4). post-refactor dogfood = PR-9d byte-identical (deterministic 확인). system-architect 의 component-level review 결론 = pre-cut nothing, all v0.18.1+ defer (kebab-normalize 흡수, Extractor dispatch unification, kebab-source-fs dep lightening 등). + - **PR #181 chore: workspace-wide cleanup + post-PR9 refactor** — ✅ 머지 (2026-05-26). v0.18.0 cut 전 마지막 정리. `[workspace.lints.clippy] pedantic = warn` + 의도적 30+ allow (각 rationale inline). 128 files mechanical clippy --fix. OMC team `post-pr9-refactor` 가 추가 H1 (`[models.nli].model` config wiring — `DEFAULT_MODEL_ID` 제거 + provider 분기) + H2 (`truncate_for_nli` stub `_hypothesis` 제거) + H3 (`was_truncated` tracing::debug! surface) + D (MCP test flake fix) + E (HOTFIXES cross-link) + 9 new tests (T1-T4). post-refactor dogfood = PR-9d byte-identical (deterministic 확인). system-architect 의 component-level review 결론 = pre-cut nothing, all v0.18.1+ defer (kebab-normalize 흡수 — v0.19.0 closure, see HOTFIXES.md 2026-05-26; Extractor dispatch unification; kebab-source-fs dep lightening 등). ## Post-merge 핫픽스 머지 후 발견된 버그들과 그 follow-up PR들은 [HOTFIXES.md](HOTFIXES.md)에 dated 로그로 기록한다. 원래 task spec은 frozen 상태로 두고, post-merge 동작 변경은 HOTFIXES.md를 source of truth로 본다. +## Future work / deferred + +- v0.20+ image/pdf normalize integration — design §3.7b intent 미구현 (3 dead struct `ParsedImageRegion` / `ParsedPdfPage` / `ParsedAudioSegment` 보존). PR #186 (normalize-absorption) 의 spec §11 (`docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md`) 참조. + ## 모든 task 공통 규약 - 의존성 경계 (`Allowed` / `Forbidden`) 위반 금지. report §19 참조.