feat(p10-1A-2): Rust AST chunker — tree-sitter-rust 코드 색인 활성화 #140
Reference in New Issue
Block a user
Delete Branch "feat/p10-1a-2-rust-ast-chunker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
개요
p10-1A-1 프레임워크 위에 Rust AST chunker 를 올려
.rs파일 end-to-end 코드 색인을 활성화. 머지 시점부터 kebab 자기 자신 dogfooding 가능.frozen design:
docs/superpowers/specs/2026-05-15-kebab-code-ingest-design.md§1A-2.task spec (frozen):
tasks/p10/p10-1a-2-rust-ast-chunker.md.plan:
docs/superpowers/plans/2026-05-19-p10-1a-2-rust-ast-chunker.md.동결된 설계 결정
kebab-parse-code), chunker 아님. design §6.3 의존성 그래프 (kebab-parse-code → tree-sitter, tree-sitter-rust) 가 authoritative. PDF 선례와 동형 — parser 가 구조화된 block 을 만들고 chunker 가 1:1 매핑. §9.1 의 "chunker 가 AST" 서술은 oversize fallback split 만 chunker-side 라는 의미로 해석.SourceSpan::Code { line_start, line_end, symbol, lang }내부 variant 신설 (kebab-core). chunks 테이블의source_spans_json은 내부 저장이라 wire schema 아님 → wire major bump 불필요.Citation::Code(wire) 는 1A-1 에서 이미 추가됨.citation_helper::citation_from_first_span에 새 arm 추가로 symbol / lang 이 chunk → SearchHit 까지 자연스럽게 흐름.MediaType::Code(String)신설 — String = canonical code_lang. 1A 는"rust"만 실제 처리, 그 외 인식된 code lang 은Skipped.frozen design §3.4 (SourceSpan) + §3.5 (MediaType) + §10.1 (post-merge surface) 동시 갱신.
변경 요약
kebab-parse-code:tree-sitter0.26 +tree-sitter-rust0.24 도입,rust.rsExtractor — tree-sitter walk 으로function_item/struct_item/enum_item/union_item/trait_item/type_item/macro_definition/impl_item(per inner fn) / 중첩mod_item재귀 처리 + glue (use/extern/const/static/mod 선언/attr/macro_invocation) 를 단일<top-level>/<module>단위로 그룹핑. 심볼 경로는 design §3.4 Rust 컨벤션 (mod::Type::method, trait 우선). 선행 doc comment 와 attribute 가 자기 unit 으로 fold 되고 중복 집계 없음 (회차 1·2 review fix).kebab-chunk:code-rust-ast-v1chunker — 1 Block::Code → 1 Chunk,AST_CHUNK_MAX_LINES(200) 초과 시 paragraph 경계로 split +<symbol> [part i/N]라벨. tree-sitter import 0 건 (boundary §6.3).pdf-page-v1와 동일 blake3 policy_hash (cross-chunker 동일성 테스트 포함).kebab-core:SourceSpan::Code+MediaType::Code신규 variant. 모든 exhaustive match 명시적으로 갱신 (catch-all 미도입).kebab-search:citation_helper가SourceSpan::Code → Citation::Code매핑.kebab-source-fs:.rs→MediaType::Code("rust")라우팅.kebab-app:ingest_one_code_asset(ingest_one_pdf_asset의 line-for-line 미러 — incremental skip / vector orphan purge / put_asset/document/blocks/chunks / embed 모두 보존), dispatch arm 추가,backfill_code_lang+backfill_repopost-search (1A-1 가 추가만 하고 미배선이던 두 SearchHit 필드 채움). e2e 통합 테스트 3 케이스.kebab-store-sqlite:code_lang_breakdown()쿼리.kebab-app/schema.rs:schema.v1.code_lang_breakdown실제 채움.tests/code_rust_ast_snapshot.rs+ JSON baseline), Task 6/7 inline 유닛 테스트, Task 8 e2e 통합 테스트, regression 케이스 (label scope / attribute dedup / module-prefix glue / determinism).AST_CHUNK_MAX_LINES상수 vs config 편차 —Chunkertrait 이 per-medium config 미노출. default 와 동일 (200) 이라 user-visible 영향 없음. 적합한 해결은 per-medium chunker registry (P+).SourceType::Codedeferred — code 파일은SourceType::Note로 분류.MediaType::Code기반 filter 는 정상 동작. 적합한 해결은 enum 에Codevariant 추가 (additive 후속).0.6.0 → 0.7.0(도그푸딩 가능 = bump 트리거, design §10.4). cascade 자동.acceptance criteria 충족
cargo test --workspace --no-fail-fast -j 1— green (단 한 가지 주의 사항 ↓).cargo clippy --workspace --all-targets -- -D warnings— clean.Citation::Code의 symbol / line 이 spec §3.4 컨벤션 일치.kebab search --json이citation.kind == "code",code_lang == "rust", (.git있는 경우)repo반환.kebab schema --json의code_lang_breakdown에"rust".media_breakdown에"code".알려진 주의 사항
crates/kebab-eval/tests/runner.rs::runner_lexical_is_deterministic_per_query_payload가 full-suite 첫 실행에서 간헐적으로 실패 (elapsed_ms: 0vs1비교) 후 isolated 재실행 시 통과. 1A-2 변경과 무관 (해당 파일 본 브랜치에서 미수정,git log main..HEAD -- crates/kebab-eval/tests/runner.rsempty). pre-existing timing flake — 별도 정리 권장 (이번 PR 범위 밖).커밋 (17)
🤖 Generated with Claude Code
TDD: red → green cycle confirmed. New `Code(String)` variant serializes as `{"code":"rust"}` via serde `rename_all = "lowercase"`. All exhaustive `match` sites updated (`media_label`, `ingest_one_asset` catch-all → explicit or-pattern). Design §3.5 enum listing synced. Also fix `/target` symlink gitignore pattern so integration-test binary lookup via workspace-relative path works with CARGO_TARGET_DIR redirect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Add `MediaType::Code("rust")` dispatch arm in `ingest_one_asset`, `ingest_one_code_asset` fn (faithful mirror of `ingest_one_pdf_asset`), and `backfill_code_lang` post-processing in `App::search_uncached`. Integration test `code_ingest_smoke.rs` verifies full pipeline: ingest `.rs` → Citation::Code hit with lang/symbol/line_start. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — 전반적으로 탄탄한 PR이지만 docs에 actionable 오류 3건 + ARCHITECTURE Mermaid 에지 오류 1건이 있어 REQUEST_CHANGES.
tree-sitter-rust parser-side / chunker tree-sitter-free 라는 의존성 경계가
cargo tree로 검증됨 (kebab-chunk → tree-sitter 없음, UI crate → kebab-parse-code 없음).backfill_repoHashMap deduplication,split_oversizeblank-line heuristic, snapshot 테스트 포함 TDD 흔적 모두 양호. HOTFIXES.md 양방향 cross-link도 완성됨.수정 필요 항목: (1)
docs/SMOKE.md에parser_version = "code-rust-ast-v1"으로 잘못 기재 — 실제 상수는"code-rust-v1"(PARSER_VERSION 확인); (2) 같은 파일에서citation.code_lang참조 2곳 — 와이어 shapeCitation::Code.lang은lang필드이므로 jq path 오류; (3)docs/ARCHITECTURE.mdMermaid에pcode --> ptypes에지가 추가됐지만kebab-parse-code는kebab-parse-types를 실제로 의존하지 않음 (cargo tree확인). 추가로 directory tree 설명에 "code-rust-ast-v1 chunker" 가 kebab-parse-code 항목에 기재되어 있으나 chunker는 kebab-chunk 소속 — 혼란 야기.runner_lexical_is_deterministic_per_query_payload플레이크 확인:git log main..HEAD -- crates/kebab-eval/tests/runner.rs가 empty → 이 브랜치에서 해당 파일 미터치, 안전.칭찬 — backfill_repo deduplication 패턴: HashMap cache 로 동일 doc_id 에 대한 중복 store 조회를 방지하는 패턴이 정확하고 comment 도 명료함.
None도 cache 에 저장해 not-found / no-repo 케이스의 중복 조회를 막는 것까지 챙겼고 (doc_id → Option<String>where None means "not found / no repo"), search result set 이 k ≤ 20 으로 작아 HashMap overhead 도 무시 가능 수준 — 설계 판단이 합리적.@@ -780,0 +824,4 @@.or_insert_with(|| {self.sqlite.get_document(&hit.doc_id).ok()concern — store 오류 묵인:
self.sqlite.get_document(...).ok().flatten()에서.ok()가Result::Err(SQLite 오류, 트랜잭션 충돌 등)를None으로 변환해 silently 삼킴. doc comment 에 "document not found" 케이스만 언급하지만 실제 I/O 오류도 같은 경로로 처리됨. 검색 결과에서repo가None으로 표시되는 문제를 디버깅하기가 어려워짐. 최소한tracing::warn!한 줄 추가하거나, PDF/image pipeline 처럼 오류를 bubbling 하는 것을 권장. 현재 repo 는 non-critical 필드이므로warn+ continue 패턴이 현실적인 절충안.@@ -15,8 +15,10 @@//! embedder, the retriever, the LLM, the RAG layer, or the UI layers.//! It consumes `CanonicalDocument` purely through `kb-core` types.mod code_rust_ast_v1;칭찬 — dep boundary 깔끔하게 유지:
kebab-chunk에 tree-sitter 의존성 없음 (cargo tree -p kebab-chunk --depth 1검증). 모듈 doc comment (//! tree-sitter is intentionally NOT a dependency here) 와 설계 근거 (design §6.3) 가 일치하고, snapshot test 도 extractor 없이 Block::Code 를 직접 생성해 경계를 유지. UI crate (cli/tui/mcp) 도 kebab-parse-code 를 직접 의존하지 않음 — 경계 구조 전체가 설계 의도와 일치.@@ -0,0 +249,4 @@units.push((format!("{prefix}{name}!"), s, e, true));}}"impl_item" => {edge case 미문서화 — fn 없는 impl 블록 전체 누락:
impl_itemarm 은 body 안의function_item만 units 에 추가하고, 해당 impl 블록 자체는 glue 에도 units 에도 들어가지 않음 (flush_glue후 units.push 없이 종료). 결과적으로impl Default for Foo { ... }처럼 fn 이 없는 (const/type만 있는) impl 블록은 색인에서 완전 누락됨 — 이는 의도된 1A scope 제한이지만 현재 테스트에 없음. module-level doc comment (//!) 에 언급되지도 않음. 적어도 file-level doc comment 에 한 줄 추가하거나,extract_inline테스트에impl Display for X { fn fmt(…) {} }vsimpl Default for X {}케이스를 추가해 의도를 명시하는 것을 권장.@@ -95,6 +98,7 @@ flowchart TBppdf --> ptypespimg --> ptypespaud --> ptypespcode --> ptypes버그 — 존재하지 않는 의존성 에지:
pcode --> ptypes가 Mermaid 다이어그램에 추가됐지만cargo tree -p kebab-parse-code --depth 1결과에kebab-parse-types가 없음. 실제로 kebab-parse-code 는 kebab-core 를 직접 의존하고 kebab-parse-types 는 의존하지 않음 (다른 parse-* crate 들은 ptypes 를 거침). 이 에지는 제거해야 함.@@ -168,6 +172,7 @@ kebab/│ ├── kebab-eval/ # golden query runner + metrics (P5-1, P5-2)│ ├── kebab-parse-image/ # ImageExtractor + Ollama OCR + caption (P6)│ ├── kebab-parse-pdf/ # lopdf per-page text extractor (P7-1)│ ├── kebab-parse-code/ # tree-sitter Rust AST extractor + code-rust-ast-v1 chunker (P10-1A-2)nit — 설명 오류: directory tree 에
kebab-parse-code항목이# tree-sitter Rust AST extractor + code-rust-ast-v1 chunker (P10-1A-2)로 기재됨.code-rust-ast-v1chunker 는kebab-chunk소속 (crates/kebab-chunk/src/code_rust_ast_v1.rs) 이므로 여기에 포함시키면 혼란을 야기함.# tree-sitter Rust AST extractor (P10-1A-2)로 줄이고 kebab-chunk 항목에code-rust-ast-v1이미 추가됐으니 충분.@@ -305,0 +313,4 @@# 2) ingest — .rs 가 code-rust-ast-v1 로 처리됨KB ingest# 3) 결과 검증 — IngestReport.items 에 .rs 자산이 "new" 로 분류, parser_version = "code-rust-ast-v1"버그 — parser_version 문자열 오류: 주석에
parser_version = "code-rust-ast-v1"으로 기재되어 있지만, 실제 상수는crates/kebab-parse-code/src/rust.rs의pub const PARSER_VERSION: &str = "code-rust-v1";임.chunker_version = "code-rust-ast-v1"과 혼동된 것으로 보임. smoke 실행 시 이 주석을 따라 확인하면 틀린 값을 기대하게 되므로 수정 필요:parser_version = "code-rust-v1".@@ -305,0 +317,4 @@KB --json ingest | jq '[.items[] | select(.doc_path | endswith(".rs"))]'# 4) 코드 검색 — code_lang 필터KB search --mode hybrid "RustAstExtractor" --code-lang rust --json | jq '{hits: [.hits[] | {symbol: .citation.symbol, code_lang: .citation.code_lang, repo: .repo}]}'버그 — jq path 오류:
.citation.code_lang는 존재하지 않는 필드.Citation::Codewire shape (kebab-core/src/citation.rs) 의 언어 필드는lang— 즉.citation.lang이 올바름.SearchHit에는 top-level.code_lang이 있으나 그건 별도 필드. jq 표현을{symbol: .citation.symbol, lang: .citation.lang, code_lang: .code_lang, repo: .repo}로 수정하면 두 필드를 구분해서 확인 가능.@@ -332,6 +369,7 @@ rm -rf /tmp/kebab-smoke # 통째로 정리- (P6-4) `image.ocr.enabled = true` + `image.caption.enabled = true` 인 워크스페이스에 PNG 가 N장 있으면 ingest 시간 ≈ markdown_time + N × (OCR + Caption latency). `gemma4:e4b` + 192.168.0.47 로 자산당 ~5-10초. 다수의 책 페이지를 이미지로 넣지 말 것 — 책은 P7 PDF 라인 사용 권장.- (P7-3) `config.chunking.chunker_version` 는 markdown 만 represent — PDF 자산은 `pdf-page-v1` 하드코딩. `config.toml` 의 `chunker_version = "md-heading-v1"` 을 봐도 PDF 는 영향 안 받음. HOTFIXES `2026-05-02 P7-3` entry 참조 (P+ chunker registry task 까지 유지).- (P7-3) 한 PDF 가 N 페이지면 `kebab ingest` 가 N 개 (또는 그 이상의, 페이지 길면 multi-chunk) 의 chunk 를 한 transaction 안에서 commit. 500 페이지 책 → 500+ chunk 한 번에 → embedding throughput 가 bottleneck. 임베딩 활성 워크스페이스에서 큰 PDF 를 처음 ingest 하면 분-단위 시간 + WAL 크기 증가 가능 — P+ 스케일 hardening task 까지 정상 동작이지만 비용은 측정 가능.- (P10-1A-2) `.rs` 파일을 워크스페이스에 두면 `kebab ingest` 결과에 `new` 카운터에 포함. `kebab search --mode hybrid "<함수명>" --code-lang rust --json` 가 `citation.kind = "code"`, `citation.code_lang = "rust"`, `citation.symbol` (함수/타입 이름), `citation.line_start` / `citation.line_end` 를 반환하면 wiring 정상. `kebab schema --json | jq .stats.code_lang_breakdown` 에 `"rust": N` 이 나오면 chunk 가 색인됨.nit — 같은 citation.code_lang 오류 재등장: 검증 체크리스트 항목에도
citation.code_lang = "rust"라고 기재됨. 위 step 4 수정과 함께 여기도citation.lang = "rust"로 정정 필요.회차 2 — 회차 1 지적 7건 모두 반영 확인. 본 회차는 safety/correctness 관점에서 PR 전체를 재검토한 결과를 담습니다.
회차 1에서 수정된 항목을 diff 수준에서 직접 검증했습니다: SMOKE.md의
.citation.code_lang→.citation.lang세 곳 전부,parser_version = "code-rust-v1"한 곳, ARCHITECTURE.md Mermaid의pcode → ptypes→pcode → core교체 및 디렉토리 트리 설명 정정,app.rsbackfill_repo의tracing::warn!(target: "kebab-app", 비-abort 보존),rust.rsimpl arm 상단 주석 추가 — 전부 의도대로 반영됨을 확인했습니다.회차 2에서 발견된 주요 사항: README.md 명령 표의 citation 설명에서 동일 클래스의 오류(wire citation 필드명 혼동)가 잔존합니다. SMOKE.md의
[ingest.code]설정 블록 참조 설명에 항목 누락이 있습니다. 나머지 correctness 항목(chunk_id 충돌 가능성, HashMap 반복 순서 유출,try_skip_unchangedparity, 빈 파일 처리,unwrap사용처)은 모두 안전함을 확인했습니다.@@ -3,3 +3,4 @@.claude//target/target/**/*.rs.bk[nit]
/target행이 추가되었지만 기존/target/행과 중복입니다. git은 디렉토리에 대해 두 패턴을 동일하게 취급합니다. 어느 한 쪽을 제거하면 됩니다(기존/target/유지가 자연스럽습니다).[doc / correctness] 회차 1에서 SMOKE.md의
.citation.code_lang→.citation.lang세 곳을 모두 수정했는데, README 명령 표에서 동일 클래스의 오류가 잔존합니다.현재:
citation.kind = "code"에symbol+code_lang = "rust"+repo(workspace root 상대) 포함code_lang은SearchHittop-level 필드이고repo도 마찬가지입니다. Citation wire 내부 필드명은lang입니다 (Citation::Code { lang, symbol, line_start, line_end, path }). 현재 문장은 사용자가citation.code_lang으로 jq를 작성하도록 오해를 유도합니다.제안 수정:
코드 chunk 는 citation 에symbol+lang = "rust"가 포함되고, SearchHit 최상위에code_lang = "rust"+repo(workspace root 상대) 가 함께 반환됨.(SMOKE.md 검증 스크립트가
.citation.lang을 올바르게 사용하고 있으니 해당 표현에 맞추면 됩니다.)@@ -0,0 +55,4 @@fn extract(&self,ctx: &kebab_core::ExtractContext<'_>,bytes: &[u8],[edge-case / nit]
line_comment/block_comment노드는walk()내부 match 에서 어느 arm 에도 해당하지 않아_ => {}(no-op)로 처리됩니다. 즉 주석만 담긴 Rust 파일(또는 빈 파일)은 블록이 하나도 생성되지 않아 ingest 후chunk_count = 0으로 기록되고 검색에 surface 되지 않습니다.동작 자체는 합리적(의미 단위 없는 파일은 검색 대상 외)이고 빈 파일의 경우도 tree-sitter 가 정상 파싱하므로 패닉은 없습니다. 다만 이 동작은 어디에도 문서화되어 있지 않아 향후 기여자가 버그로 오인할 수 있습니다.
build_blocks함수 상단이나 모듈 doc-comment에 한 줄 추가를 제안합니다:// A comment-only or empty file produces zero blocks (no semantic units).[doc / nit]
[ingest.code]설정 (config.toml 에 이미 포함됨 — 위 격리 config 블록 참조) 라고 하지만, 위 격리 config 블록(smoke setup 절의 toml 예시)에는skip_generated_header/max_file_bytes/max_file_lines세 항목만 있습니다. 바로 아래에 보여주는 4항목 블록의extra_skip_globs = []는 격리 config 블록에 없는 항목입니다.extra_skip_globs의 기본값이[]이라 동작에는 문제 없지만, 독자가 「격리 config에 이미 포함됐다고 했는데 내 파일엔 없네?」 하고 혼동할 수 있습니다. 아래 두 가지 중 하나로 해결하면 됩니다:extra_skip_globs = []행을 추가하거나,기본값 참조 — 필요한 경우 config.toml 에 추가).회차 3 — 회차 2 수정 4건 모두 확인 완료 (README wire 필드 구조, SMOKE 격리 config
extra_skip_globs, rust.rs edge-case doc comment,.gitignore/targetdedup). 신규 actionable 없음 — 교차 문서 버전 일관성 (0.7.0,code-rust-ast-v1,code-rust-v1, HOTFIXES 날짜2026-05-19) 전부 정합,tracing::warn는 실제 DB 오류 시에만 발생 (정상 검색 시 무음), jq 예시 및 wire 필드 표현 정확. APPROVE.@@ -780,0 +808,4 @@/// Non-repo documents (markdown, PDF, plain text, code files outside a/// git tree) correctly keep `repo: None` — `Metadata.repo` is already/// `None` for those, so the assignment is a no-op.fn backfill_repo(&self, hits: &mut [SearchHit]) {HashMap<DocumentId, Option<String>>dedup 캐시로 동일 doc 의 중복 store 쿼리를 방지하고,Ok(None)케이스 (repo 없는 문서) 도 캐시에 기록해 re-query 를 회피하는 설계가 깔끔합니다.Err분기에서tracing::warn을 남기고 검색 응답 전체는 abort 하지 않는 non-aborting 정책도 review round 1 피드백을 정확히 반영했습니다.@@ -0,0 +153,4 @@/// gluing paragraphs until ~`AST_CHUNK_MAX_LINES` lines accumulate./// Returns `(line_offset_start, line_offset_end, text)` where offsets are/// 0-based within the unit (caller adds the unit's absolute `line_start`).fn split_oversize(code: &str) -> Vec<(u32, u32, String)> {split_oversize + [part i/N] 명명 패턴이 깔끔합니다. oversize 블록을 blank-line 경계에서 분리하고
part_ls = ls + off_start로 원본 line 번호를 정확하게 보존하는 구현이 design §9.1 의 의도를 충실히 반영합니다.