Commit Graph

53 Commits

Author SHA1 Message Date
2871da14f4 feat(parse-pdf): #232 스캔 PDF 를 페이지 렌더링으로 OCR
`extract_dctdecode_page_image` 는 페이지의 image XObject 중 `/Filter` 가
정확히 DCTDecode 인 것 하나만 받는다. 실제 스캔본에서 흔한 CCITTFaxDecode·
JBIG2Decode·FlateDecode·JPXDecode, `[FlateDecode, DCTDecode]` 같은 체인,
Internet Archive 계열의 "배경 + /ImageMask" 분리 구조가 전부 걸러진다.

텍스트 게이트는 정상 동작했다. `needs_ocr` 판정을 통과했다는 건 "이 페이지는
스캔본이라 OCR 이 필요하다" 고 올바르게 본 것이다. 판정은 맞았고 래스터를 못
꺼냈을 뿐인데, 결과가 조용한 내용 손실이었다 — 색인은 성공으로 끝나고,
검색이 안 되는 시점에야 알게 되며, 그때 원인이 PDF 인코더라는 걸 역추적할
방법이 없다.

페이지를 렌더링한다 (`page_render::PageRenderer`, pdfium). 지원할 필터도,
고를 XObject 도 없고, 벡터와 이미지가 섞인 페이지도 리더가 보는 대로 나온다.
이슈가 지적한 "image XObject 선택이 비결정적" 문제도 이 경로에서는 성립하지
않는다.

이슈는 교체를 권했지만 렌더러 우선 + DCTDecode 폴백으로 갔다. 배포 형태
때문이다 — pdfium 은 공유 라이브러리로만 배포되고 정적 빌드가 없어서,
링크하면 CLAUDE.md 가 규정한 단일 바이너리가 깨진다. 사용자와 상의해 정했다.

  - 런타임 바인딩. 있으면 전 인코딩 커버, 없으면 오늘 동작 + 왜 건너뛰었는지.
  - `[ingest.pdf.ocr] render_library` 로 경로 지정, 비우면 로더 경로 탐색.
  - `kebab doctor` 의 `pdf_render` 가 어느 쪽인지 보고.
  - 바이너리 392.9 → 399.3 MB (+6.4 MB 글루). ldd 에 pdfium 없음.

조용한 손실을 시끄럽게 (이슈 부수 제안 2·3):

`failure_reason` 이 CLI 에서 `..` 로 버려지고 있었다. wire 이벤트는 원인을
구분해 싣는데 사람이 보는 출력이 "no DCTDecode or engine fail" 로 뭉갰다.
이제 no_renderer / render_error / ocr_error 를 구분해 찍는다.
`IngestReport.ocr_skipped_pages` 를 추가하고(additive) 사람용 요약에도
`ocr-skipped N` 으로 낸다 — stderr 한 줄로 흘리면 대량 ingest 에서 지나간다.

parser_version cascade: pdf-text-v1 → pdf-text-v2. 안 올리면 이미 색인된
스캔본에 적용되지 않는다 (파일이 안 바뀌었으니 해시가 같고 Unchanged 로
건너뛴다). 사용자가 --force-reingest 를 떠올려야만 고쳐지는 수정은 고쳐진 게
아니다. 스냅샷 둘이 따라 움직였고 바뀐 것이 파생 식별자뿐임을 확인했다 —
본문 텍스트·inlines·source_span·metadata 는 동일.

구현 중 발견: pdfium 은 동시 사용이 안전하지 않다. 테스트를 병렬로 돌리자
`double free or corruption` 으로 프로세스가 죽었고, `thread_safe` 기능만으로는
부족했다. ingest 는 PDF 를 하나씩 처리하니 오늘은 문제가 없지만 `Arc` 는
공유해도 된다고 광고하는 타입이라, `PageRenderer` 안에 뮤텍스를 두고
`RenderedPdf` 가 문서 수명 동안 잡게 했다 (필드 선언 순서가 load-bearing —
doc 이 guard 보다 먼저 드롭돼야 한다). 지금 비용 0, 병렬화되는 날 메모리
손상 대신 대기가 된다. `set_target_width` 만 주면 긴 스캔에서 pdfium 이 C++
length_error 로 프로세스를 죽여서(exceptions 비활성 빌드라 Err 로 못 받는다)
양변을 set_maximum_* 으로 묶었다. 바인딩도 run 당 1회여야 한다.

실측 (govdocs1-000157-ccitt.pdf, 22쪽 중 1쪽이 CCITT 스캔, gemma3:4b):

                  렌더러 없음                        렌더러 있음
  OCR        ⊘ 건너뜀 — 인코딩을 읽을 수 없다    ✓ 101 chars, 6489ms
  chunk                35                              36
  글자 수            35,994                          36,095
  요약           ocr-skipped 1                        (없음)

렌더링 자체는 여섯 필터 계열 전부 확인 — CCITT / JBIG2 / Flate / JPX /
혼합(DCT+CCITT+JBIG2+Flate) / DCT, 300dpi 페이지당 40~145 ms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-17 01:53:46 +09:00
745ac6e589 docs: README/ARCHITECTURE v0.32.0 동기화 + CLAUDE.md README 최신성 규칙
최신 코드베이스 기준으로 사용자/아키텍처 문서 갱신:

README (사용자 설명서 수준):
- 버전 스탬프 v0.21.0 → v0.32.0 (2곳)
- 파생물 캐시 설명에 OCR/caption 캐싱 반영 (embedding 만 → embedding+vision)

ARCHITECTURE (구조 상세):
- migrations V001..V014 → V015 (drop chat_sessions, v0.31.0)
- derivation_cache kind: "embedding 만 남음" → embedding + ocr + caption (#217)
- 9개 code_*_ast_v1.rs → 단일 code_ast_v1.rs / CodeAstV1Chunker (v0.32.0 #220) — dir tree + 결정표
- MCP 도구 목록 4개 → 8개 (schema/doctor/search/bulk_search/ask/fetch/ingest_file/ingest_stdin)
- stale /build/dogfood 경로 → 실 경로/HOTFIXES 참조

CLAUDE.md:
- "README 최신성 hard requirement" 명문화 — 기능 추가/변경/삭제·사용자 표면 변경 시
  그 PR 에서 README 동기화, 제거된 surface 는 README 에서도 제거. 단 깊이는
  "사용자 설명서" 수준이고 아키텍처는 docs/ARCHITECTURE.md 에 — altitude 경계 명시.
- stale TUI bullet 정정 (kebab tui 는 v0.31.0 제거 → CLI 가 유일 UI surface)

code: derivation.rs 모듈 주석의 kind 목록 {embedding,alias,korean_tokens} →
{embedding,ocr,caption} (source-of-truth 주석 정정).

독립 doc-accuracy 감사로 config 기본값(llm/embedding/rag/chunker/ocr)·CLI 표면·
crate 목록·migration·cache kind 전수 대조 — 위 2건 외 잔여 staleness 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 11:31:27 +00:00
2d68827cf5 refactor(core): 빈 re-export shim crate kebab-embed/kebab-llm → kebab-core 흡수
kebab-embed/kebab-llm 은 "새 type 없음"을 자처한 순수 re-export 셸이었다
(trait 은 이미 kebab-core 소유, mock + test helper 만 보유). "kebab-core
재구성 시 안정 surface" 라는 명분은 1인 RAG 엔 speculative YAGNI. 흡수:

- kebab-core 에 default-OFF `mock` feature + src/mock.rs (MockEmbedder,
  MockLanguageModel, assert_vector_shape/assert_unit_norm/assert_finish_chunk
  을 kebab_core:: → crate:: import 만 바꿔 verbatim 이동).
- production import 2곳(kebab-embed-local, kebab-llm-local) +
  test import 다수(search/rag/parse-image/embed-local) 를 kebab_core 로 repoint.
  mock 쓰는 crate 는 dev-dep 에 features=["mock"] (default 빌드 무영향).
- shim 자체 테스트: mock 동작 테스트는 kebab-core/tests/ 로 이동,
  reexports.rs(셸 재수출 테스트)는 폐기.
- crates/kebab-embed, crates/kebab-llm 삭제 + workspace member/deps 정리.
- ARCHITECTURE/HANDOFF/component README 의 crate 그래프·표·rationale 갱신
  (22 → 20 crates). llm-local 의 broken intra-doc link 2건도 정리.

trait surface·동작 불변 (test-only + import-rename). workspace build 는 mock
default-OFF 라 mock 코드 미컴파일. 적대적 검증 3렌즈(build-test-integrity +
behavior-identity[mock byte-identical] + dead-crate-completeness) 통과,
clippy --workspace -D warnings 클린.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 01:06:14 +00:00
6ab503e27f feat(app): 이미지 OCR 결과 캐시 — 바이트 키 ocr 네임스페이스 (b1) 2026-06-25 00:54:20 +00:00
7ed0151b81 feat(core): derivation_cache_key_bytes — 바이트 키 변형 (OCR/caption 입력용, NFC 없음) 2026-06-25 00:38:03 +00:00
2bbe2f8ace refactor(app): markdown을 extractor registry 경유로 통일 (extract stage 대칭화)
markdown ingest arm 이 그동안 유일하게 `App::extract_for` extractor
registry 를 우회하고 `kebab_parse_md::{parse_frontmatter, parse_blocks,
build_canonical_document}` free function 을 직접 호출했다 ("the single
biggest asymmetry"). 이를 image/pdf/code 와 동일하게 registry 경유로
통일.

- `kebab-parse-md` 에 `MarkdownExtractor` 신설 — 기존 free function 3종을
  동일 순서·동일 인자로 감싸 `bytes → CanonicalDocument` 생산만 담당.
  fm_span_end / count_lines_in / build_body_hints 헬퍼도 함께 이식.
- `App.extractors` registry 에 등록 (11 → 12 entry), markdown 이 `supports`
  로 발견되도록 첫 entry 로 배치.
- `ExtractContext` 에 `source_id` / `source_trust` 필드 추가 — markdown
  frontmatter 가 per-source trust 기본값을 override 하고 그 precedence 가
  `parse_frontmatter` *내부*에서 결정되므로 ctx 가 carry 해야 함. 다른
  extractor 는 None (post-extract 에서 source_id stamp 유지).
- 핸들러는 추출 stage 만 registry 로 이전 — version stamping / chunking /
  embedding / store 는 그대로. IngestItem.warnings 는 pdf/code 처럼
  `canonical.provenance` 의 Warning 이벤트에서 도출.

byte-identical 검증: parity gate-ingest (all-markdown 183 doc / 7676 chunk)
CHUNKS / SEARCH / ASK 모두 IDENTICAL. clippy 0, kebab-app + kebab-parse-md
test 전체 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 13:30:54 +00:00
1d32aa645a refactor: multi-turn 세션 제거 (ask --session, chat_sessions/turns) + V015 drop migration 2026-06-24 09:57:12 +00:00
24ec86c515 feat(rag): rag-v4 — RAG provenance 라벨 (source/trust + 신뢰도 우선 지시)
RAG 프롬프트의 각 [근거] 청크 머리에 출처/trust 라벨을 붙이고
(`[#n] source=jira trust=secondary doc=…`), system prompt 에 "저신뢰 출처를 권위
출처와 충돌 시 discount 하고 [#번호]로 귀속" 2규칙을 더한다. 출처 필터
(`--source`/`--trust-min`)가 못 잡는 생성 측 실패 — 저신뢰(jira) 청크가 권위(wiki)
청크를 답변에서 덮어쓰는 것 — 를 다룬다.

- SearchHit 에 source_id/trust_level(additive optional). lexical/vector build_hit
  가 documents 조인에서 채움(both 동일: trust_level lowercase TEXT →
  serde lowercase round-trip, doc_summary read-back 과 동형). hybrid fusion 전파.
- pack_context 라벨 렌더(버전 무관 항상). SYSTEM_PROMPT_RAG_V4 = rag-v3 8규칙
  verbatim + 2규칙. config 기본 rag-v3→rag-v4. multi-hop synth 도 2규칙 →
  rag-multi-hop-v1→v2(prompt 변경 = 버전 bump, design §9).
- wire: search_hit.v1 에 두 필드 optional additive(required 아님,
  skip_serializing_if=None → 구 소비자 무영향, v2 bump 아님).
- source_id 는 RAG 헤더에 렌더되므로 validate_sources 에 [A-Za-z0-9._-] char 검증.
- opt-out: rag-v3 핀 = v3 system prompt 선택(discount 지시 빠짐, 라벨은 무해히 잔존).

검증: kebab-core/search/rag/config/eval 전 테스트 green(25 바이너리), clippy 0.
독립 코드 리뷰 APPROVE(7위험 PASS — trust round-trip 실 DB 확인; MEDIUM rag-v3
opt-out doc + multi-hop 버전 / LOW source_id 검증 반영). 도그푸딩: 라벨 메커니즘
end-to-end 검증(search --json 이 competing 쿼리에 wiki/primary + jira/secondary 둘
다 정확 라벨로 노출). LLM-judge(답변 비교)는 instruction LLM 부재로 보류(.2/.47
다운 + lemonade /api/generate it-model template 미적용) — 인프라, .2 복구 시 측정.
버전 bump 은 follow-up 들과 배치 릴리스에서 일괄.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 05:48:34 +00:00
58ac62d53a feat(search): provenance 출처 필터 — [[workspace.sources]] 멀티소스 + --source/--source-type
혼합 출처 KB(위키+jira 등)에서 색인은 전부 하되 질의 시 출처로 좁히는 provenance
레버. 전역 trust 곱셈가중(weighted-RRF)은 A/B 에서 반증(θ=0.85 만으로 incident MRR
0.918→0.340 절벽, 점수 압축) — 필터가 see-saw 없는 올바른 레버.

- config [[workspace.sources]] (각 id/root/exclude/trust_level/source_type);
  단일 root 는 implicit `default` source 로 정규화. validate: id 유일·비어있지 않음.
- config schema v3→v4 (step_3_to_4, root→[[workspace.sources]] id=default 미러, 멱등)
- V014 documents.source_id 컬럼+인덱스 (additive, DEFAULT 'default', 재색인 0)
- Metadata.source_id + BodyHints trust precedence(frontmatter > source 기본값 > Primary)
- ingest: --root 미지정 시 resolved_sources() 순회 + doc 마다 source_id/trust stamp
- 검색 SearchFilters.source_type/source_id → lexical + vector 두 site (IN, OR)
- CLI kebab search --source <id> / --source-type <type> (repeatable/comma-sep)

도그푸딩(620 doc, jira400+wiki220): --source wiki 로 개념 질의 MRR 0.780→0.810,
--source jira 로 incident 0.918→0.975. trust precedence 실측(jira=secondary 기본값).

version bump 0.28.0 → 0.29.0 (신규 CLI flag + config 키 + V014 migration → minor).
follow-up: MCP search 필터 미노출 · kebab list source_id 미표시 · RAG provenance 라벨.

자세한 내용: tasks/HOTFIXES.md (2026-06-21), docs/release-notes/v0.29.0-draft.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-21 08:35:19 +00:00
b1c5feb3f3 refactor(core): Chunk.aliases 필드 제거
doc-side expansion(별칭) 제거 — Chunk 의 aliases: Option<String> 필드와
serde default 테스트 제거. Metadata.aliases(Vec, 문서 메타)는 유지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:36:44 +00:00
a8fd76499c feat(expansion): doc-side expansion 별칭 개별 dense 벡터 + 파생물 캐시(V012)
별칭을 줄별 개별 dense 벡터(sentinel `{chunk}#alias#N`)로 색인하고
boilerplate 청크는 별칭 생성을 skip. 묶음 1벡터 방식은 평균화로 특정
표현이 희석돼 오히려 회귀(13/18)했던 것을 폐기. 변형 일관성 14/18 →
16/18, mean_spread@10 0.222 → 0.111 (나무위키 ~1000 문서 CS corpus).
`kebab-core::strip_alias_suffix` 가 suffix 형과 per-alias 형 둘 다 처리.

파생물 캐시(V012): embedding 벡터 + 별칭 LLM 결과를 청크 내용 해시
키로 캐싱해 재색인 시 내용 불변 청크의 재계산을 skip. cache_key =
blake3(kind ‖ text_blake3 ‖ version_key)[:32], version_key 에
model/prompt/dimensions 포함 → §9 cascade 와 정합(버전 bump 시 자동
miss). 측정: 정답 3개 cold 1879s → warm 13s ≈ 145배. 순수 가산이라
corpus_revision bump 없음. search/ask 는 kebab.sqlite+lancedb 만으로
동작 → 외부 서버 색인 후 DB 만 복사하는 이식 워크플로 가능.

V012 schema migration + 신규 surface 로 workspace version 0.20.2 →
0.21.0 (minor) bump. README/HANDOFF/ARCHITECTURE/HOTFIXES sync.
known limitation: stack·svm 설명형 2개 잔존 + grounded 판정이 부분
인용을 grounded 로 오분류(후속 후보).

측정 상세: docs/superpowers/handoffs/2026-05-31-namu-wiki-alias-cache-study.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 08:24:04 +00:00
b56469f010 fix(core): clippy uninlined_format_args — strip_alias 테스트 (리뷰 MAJOR-1)
workspace clippy --all-targets -D warnings 게이트 통과. format! 인자 인라인.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:24:04 +00:00
86b4e1ebd0 feat(core): ALIAS_SUFFIX + strip_alias_suffix (dense alias vectors) 2026-05-30 10:31:03 +00:00
848b75c069 feat(core): Chunk.aliases 필드 (doc-side expansion)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 02:09:39 +00:00
b134ae9dd5 feat(chunk): integrate lindera korean morphological tokenizer
V009 의 tokenized_korean_text column 에 들어갈 morpheme sequence
를 lindera ko-dic 으로 분해. chunk builder pipeline 의 chunk 생성
직후 시점에서 호출 → chunk struct 의 field 에 pre-fill → store
의 put_chunks 가 단일 transaction 안에서 INSERT.

- crates/kebab-core/src/chunk.rs: Chunk struct 에
  tokenized_korean_text: Option<String> field 추가 (#[serde(default)]).
- crates/kebab-chunk/src/lib.rs: tokenize_korean_morphological()
  helper + OnceLock 캐싱 + fallback (None) 정책.
- crates/kebab-chunk/Cargo.toml: lindera features = ["embed-ko-dic"]
  추가 (DictionaryKind::KoDic 활성화에 필요).
- 모든 chunker (tier2_shared, md_heading_v1, pdf_page_v1, 9개
  code AST v1): Chunk 리터럴에 tokenized_korean_text pre-fill.
- crates/kebab-store-sqlite/src/documents.rs::put_chunks: INSERT
  SQL column list + placeholder + binding 갱신 (12번째 column).
- crates/kebab-chunk/tests/tokenize_korean.rs: 단위 테스트 2개.

lindera 3.0.7 API 정정: load_dictionary_from_kind →
load_embedded_dictionary, Token.text → Token.surface.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §6.2
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:22:15 +00:00
685007789a style: cargo fmt --all (round 4 ingest log feature follow-up)
Phase C4 executor 의 마지막 `fix(test): clippy + fmt fixes` commit 이
test file 부분만 fmt 적용. workspace 전체 fmt 누락 발견 → cargo fmt --all
적용. 모든 import alphabetical reorder + line wrapping 정합.

추가 untracked artifact 동시 commit:
- docs/superpowers/specs/2026-05-28-v0.20-ingest-log-spec.md (491 line, ACCEPT)
- docs/superpowers/plans/2026-05-28-v0.20-ingest-log-plan.md (616 line, ACCEPT)

workspace test: 1370 passed / 0 failed / 50 ignored, ingest_log_smoke green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 04:18:40 +00:00
b9ee09f176 feat(app): wire PDF OCR enrichment + cancel propagation into ingest_one_pdf_asset (H-5 eager init + post-extract hook + per-page cancel) + workspace lopdf dep (Step 4 M-4)
Step 6 (Group E) of v0.20.0 sub-item 1 (scanned PDF OCR) plan +
Step 7 spillover (IngestEvent variant + IngestItem field for compile
boundary) + Step 4 reviewer Minor M-4 fix.

E1 — eager PDF OCR engine build at `ingest_with_config_opts` entry,
mirror of image OCR pattern (lib.rs:338-347). `pdf.ocr.enabled ||
always_on` 시 `OllamaVisionOcr::from_parts(endpoint, model, ...)` 호출
+ fail-fast `?`. App field 추가 0 (local var only, spec L-1 / Step 1
A1 cosmetic fix 정합).

E2 — `ingest_one_pdf_asset` signature extension: +3 param
(`pdf_ocr_engine: Option<&OllamaVisionOcr>`, `progress: Option<&
mpsc::Sender<IngestEvent>>`, `cancel: Option<&Arc<AtomicBool>>`).
`ingest_one_asset` dispatch wrapper + caller (dispatch loop) update.

E3 — post-extract enrichment block at `extract_for` 직후 (line 1779).
`pdf.ocr.enabled || always_on` 시 `apply_ocr_to_pdf_pages` 호출,
PdfOcrProgress → IngestEvent emit (PdfOcrStarted / PdfOcrFinished
with ocr_engine), summary 의 pages_ocrd/ms_total 을 IngestItem field
로 carry. PR #187 registry dispatch invariant 보존
(`extract_for(&asset.media_type, ...)` 그대로).

E4 — cancel handle propagation: ingest_with_config_cancellable →
IngestOpts.cancel → ingest_with_config_opts → ingest_one_asset →
ingest_one_pdf_asset (new `cancel` param) → PdfOcrOpts.cancel chain.
spec §4.8 line 1159 production wiring.

Step 7 spillover (compile boundary):
- `kebab_app::ingest_progress::IngestEvent`: PdfOcrStarted { page } +
  PdfOcrFinished { page, ms, chars, ocr_engine }. serde discriminant
  `pdf_ocr_started` / `pdf_ocr_finished` (Step 7 G3 wire schema 와 일치).
- `kebab_core::IngestItem`: pdf_ocr_pages: Option<u32> +
  pdf_ocr_ms_total: Option<u64> (warnings/error 사이). 11 non-PDF
  IngestItem construct site 가 `None` 채움.
- `kebab-cli/src/progress.rs` + `kebab-tui/src/ingest_progress.rs`:
  새 variant no-op handler (v1에서 per-page progress 미노출, future
  refinement 시 활성화 가능).
- `kebab-store-sqlite/tests/ingest_report_snapshot.rs` + snapshot
  `ingest_report.snapshot.json`: 2 IngestItem fixture 의 새 field 추가.
- Step 7 의 JSON Schema 갱신 + CLI printer activation + snapshot
  regenerate 는 별 commit (G3/H1/H2 deliverable).

M-4 (Step 4 reviewer Minor) — lopdf workspace dep 통합:
- workspace `Cargo.toml [workspace.dependencies] lopdf = "0.32"`.
- kebab-app + kebab-parse-pdf 의 direct dep → `{ workspace = true }`.

Verifier evidence:
- workspace test (`cargo test --workspace --no-fail-fast -j 1`):
  175 test result summary lines, 0 failures, 0 FAILED.
- workspace clippy (`-D warnings`): exit 0, 0 warning.
- dep graph baseline (`.omc/state/pdf-ocr-{parse-pdf,app-parse}-deps.baseline.txt`):
  empty diff for both.

spec:  docs/superpowers/specs/2026-05-27-pdf-scanned-ocr-spec.md (§4.4 + §4.6 + §4.8)
plan:  docs/superpowers/plans/2026-05-27-pdf-scanned-ocr-plan.md (Step 6 E1-E4 + Step 7 partial G1+G2)
prior: 4672cba (Step 5 fix) + fd918a6 (Step 5) + 9f003ef (Step 4 helper)
contract: §9 (additive minor wire bump — Step 7 JSON Schema 완료 시)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:18:34 +00:00
bd48baa19a refactor(source-fs): drop kebab-parse-code dep — 9 tree-sitter grammars drag 제거
kebab-source-fs 가 kebab-parse-code 의 9 tree-sitter grammars 를 drag 했던 무거운 의존성 제거. 4 surface (code_lang_for_path / is_generated_file / is_oversized / BUILTIN_BLACKLIST) 만 사용하지만 dep 그래프에서 9 grammar 전체 link → kebab-source-fs::code_meta 로 이전 + kebab-parse-code 측 cleanup.

핵심 변경:
- kebab-source-fs::code_meta 신설: 4 surface 이전 (BUILTIN_BLACKLIST `pub` for frozen contract + 3 helper fn `pub(crate)`). lib.rs 의 `pub use code_meta::BUILTIN_BLACKLIST` 1 줄 추가 (Option A — 다른 mod surface 무근거 확장 0).
- callsite migration: media.rs (1) + walker.rs (2) + connector.rs (2) 모두 `kebab_source_fs::code_meta::*` 로 갱신.
- kebab-parse-code 측 cleanup: skip.rs 삭제 + lang.rs narrow edit (code_lang_for_path body + unit test 2 + Path import 삭제, module_path_for_* 보존) + lib.rs 헤더 doc rewrite (migration breadcrumb 포함).
- tests/{lang,skip}.rs 13 test 이동 — 12 unit (`src/code_meta.rs::tests`) + 1 integration (`tests/code_meta.rs` for BUILTIN_BLACKLIST frozen contract).
- design §8 graph: edge 제거 + p10-2 inline note. ARCHITECTURE.md 산문 1 줄 갱신. kebab-core::metadata.rs:36 stale dep reference 정정.

G1+G5: cargo tree -p kebab-source-fs | grep tree-sitter = 0 줄.
G2+G3: workspace test 회귀 0 + 13 test 1:1 이동.
G4: design §8 + ARCHITECTURE.md 갱신.

Wire 영향: 없음 (internal Rust crate-API surface 만, user-facing 0). Cargo workspace.version bump 불필요.

Refs:
- docs/superpowers/specs/2026-05-26-source-fs-dep-lightening-spec.md (v3, 4-round APPROVE)
- docs/superpowers/plans/2026-05-26-source-fs-dep-lightening-plan.md (v4, 4-round ACCEPT)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:19:32 +00:00
7c85de065a chore: workspace-wide cleanup — clippy::pedantic baseline + auto-fix
cut PR v0.18.0 전 마지막 정리. 사용자 요청: "전체 코드베이스를 깔끔하고 알아보기 쉽게".

## Workspace lints

- `Cargo.toml` 의 `[workspace.lints.clippy]` 에 `pedantic = "warn"` (priority -1) + 의도적 allow-list 추가:
  - cast_possible_truncation / cast_possible_wrap / cast_sign_loss / cast_precision_loss — ONNX i64 / hash modular reduction 등 의도적 truncation.
  - doc_markdown / missing_errors_doc / missing_panics_doc — cosmetic doc style.
  - too_many_lines / module_name_repetitions / must_use_candidate / needless_pass_by_value / manual_let_else / items_after_statements / similar_names — informational only.
  - format_collect / match_wildcard_for_single_variants / trivially_copy_pass_by_ref / unnecessary_wraps — intentional patterns (exhaustive match, future Result variants 등).
  - default_trait_access — `Foo::default()` 가 idiomatic.
  - float_cmp — NLI / RRF score 의 explicit threshold 비교 의도.
  - struct_excessive_bools / case_sensitive_file_extension_comparisons / naive_bytecount / ignore_without_reason — domain-specific 의도.
  - format_push_string / return_self_not_must_use / match_same_arms — builder / wire-label / hot-path 패턴 보존.
  - needless_continue / used_underscore_binding / nonminimal_bool / unreadable_literal / many_single_char_names / doc_link_with_quotes / assigning_clones / collapsible_str_replace / trivial_regex / elidable_lifetime_names / range_plus_one / explicit_iter_loop / implicit_hasher / ref_option — remaining low-value style.
- 각 24 crate `Cargo.toml` 에 `[lints] workspace = true` 추가.

## Auto-fix

`cargo clippy --workspace --all-targets --fix` 적용 — 128 files changed, 552 insertions / 472 deletions. 주로:
- uninlined_format_args (~18): `format!("{}", x)` → `format!("{x}")`.
- redundant_closure_for_method_calls (~33): `.map(|x| x.foo())` → `.map(T::foo)`.
- 그 외 mechanical refactor.

## 검증

- `cargo clippy --workspace --all-targets -j 1 -- -D warnings` clean (pedantic + 모든 lint group).
- `cargo test --workspace --no-fail-fast -j 1` — **1293 tests pass + 1 pre-existing flaky fail** (`kebab-mcp::tools_call_ask_multi_hop::ask_tool_routes_multi_hop_true_to_decompose_first`, HOTFIX candidate, cleanup 무관). 회귀 0.

Wire 영향: 없음.
Behavior 영향: 없음 (mechanical refactor only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 03:01:58 +00:00
546c1564b0 feat(rag): fb-41 PR-9c-1 — core types + wire scaffolding (NLI verification)
Surface-only PR (no behavior wiring — that's PR-9c-2):
- kebab-core: RefusalReason::NliVerificationFailed + NliModelUnavailable (serde rename_all="snake_case", wire = identical strings).
- kebab-core: Answer.verification: Option<VerificationSummary> field (additive minor wire — pre-v0.18 reader 무영향).
- kebab-core: VerificationSummary { nli_score: f32, nli_threshold: f32, nli_passed: bool } struct + lib.rs 재-export.
- kebab-config: NliCfg { model, provider } + ModelsCfg.nli (default Xenova/mDeBERTa-v3-base-xnli-multilingual-nli-2mil7).
- kebab-config: RagCfg.nli_threshold: f32 (default 0.0 = disabled, spec §2.6 single gate).
- kebab-config: env override KEBAB_MODELS_NLI_MODEL/PROVIDER + KEBAB_RAG_NLI_THRESHOLD (parse 실패 시 tracing::warn + default 유지).
- kebab-rag: RagPipeline.verifier: Option<Arc<dyn NliVerifier>> field + with_verifier builder (모두 #[allow(dead_code)] — PR-9c-2 의 step 8.5 hook 가 활성화 시 제거). RagPipeline::new signature 유지 (round-2 NEW-M1 Option B).
- kebab-rag: Cargo.toml 에 kebab-nli path 의존 추가.
- kebab-store-sqlite + kebab-tui: 두 신규 RefusalReason variant 에 대한 exhaustive match arm 추가 (snake_case label / 표시 문구).
- 모든 Answer 구축 site (rag 6 + cli/tui/eval 3 fixture) 에 verification: None 추가.
- wire schemas: answer.schema.json verification field + \$defs.VerificationSummary + refusal_reason.enum 2 추가. error.schema.json code.enum + details.description 2 추가 (forward-looking reserved).
- docs/ARCHITECTURE.md: Mermaid Adapters subgraph 의 nli 노드 + rag→nli + app→nli (forward-looking) + nli→config edges. nli→core edge 는 skip (kebab-nli/Cargo.toml direct dep 가 config 만, ARCHITECTURE 컨벤션 = direct deps only). 디렉토리 트리에 crates/kebab-nli/ 추가.

Tests: kebab-core 3 (serde rename + verification skip + struct shape) + kebab-config 6 (defaults + legacy + env + malformed env) + kebab-cli wire 5 (schema verification + enum 검증).
검증: cargo test --workspace -j 1 회귀 0 (pre-existing kebab-mcp::tools_call_ask_multi_hop flaky 1개 동일 — spec 에 명시된 known-flaky). cargo clippy --workspace --all-targets -D warnings clean.
Wire 영향: additive minor — answer.v1 의 verification optional + refusal_reason.enum 확장 + error.v1.code 확장.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:27:36 +00:00
6188a50c1c feat(rag): fb-41 PR-3b-ii — ScriptedLm + 5 multi-hop tests + refusal hop trace + carry-over
PR-3b 의 분할 두 번째 PR — PR-3b-i 의 dynamic decide loop 위에서:

1. **ScriptedLm + ScriptedRetriever helper** (kebab-rag tests/common/mod.rs)
   per-call 다른 response 반환. decompose / decide×N / synthesize 의 각
   LLM call 을 구분하는 다단계 multi-hop 시나리오를 mock-only 로 exercise
   가능. `Vec<&str>` / `Vec<Vec<SearchHit>>` 받아 call sequence 순서대로
   emit. Send + Sync.

2. **5 multi-hop integration tests** (kebab-rag tests/multi_hop.rs 신규)
   - decide_stop_triggers_synthesize: decide [] → 즉시 synthesize
   - decide_continue_adds_more_chunks: decide ["q2"] → iter 2 retrieve + pool 확장
   - max_depth_force_stops: depth cap → forced_stop + decide LLM call skip
   - pool_chunks_dedup_by_chunk_id: 같은 chunk_id 두 sub-query 에서 1 회
   - decide_parse_failure_falls_through_to_synthesize: parse fail = graceful
     synthesize (refusal 아님, spec §9)

3. **refuse_* helper hops trace 보존** (회차 1 carry-over)
   refuse_no_chunks / refuse_score_gate 시그니처에 `hops:
   Option<Vec<HopRecord>>` 인자 추가. ask_multi_hop 의 score-gate /
   no-chunks refusal 시 누적된 hops 그대로 Answer.hops 에 보존.
   single-pass ask 는 None 전달 — wire 변동 없음 (skip_serializing_if).

4. **HopRecord doc 보강** (회차 1 carry-over)
   sub_queries 의 per-kind 의미 명시 (Decompose=initial / Decide=next-iter
   or empty=stop / Synthesize=always empty). llm_call_ms=0 의 ambiguity
   (no call vs 0ms call) doc 명시.

5. **MULTI_HOP_MAX_SUB_QUERIES_DEFAULT → _HARD_CAP rename** (회차 1 carry-over)
   const 의 의도 명확화 — config knob `multi_hop_max_sub_queries_per_iter`
   (5, prompt-side soft hint) 와 const (10, parse-side hard ceiling)
   분리. 두 layer 의 책임 doc 동기화. test 도 rename.

6. **decide guard 단순화 + preview budget doc** (회차 1 carry-over)
   parse_decompose_response 의 post-condition (Some=non-empty 보장)
   doc 명시. defensive `Some(qs) if !qs.is_empty()` →
   `decide_result.unwrap_or_default()` 단순화. decide preview 의
   snippet-only path (full chunk text 안 fetch) 의도 doc.

검증
- `cargo test -p kebab-rag -j 1` — 31 unit + 19 pipeline + 5 multi_hop
  + 3 prompt_template + 3 streaming 모두 통과.
- `cargo clippy -p kebab-rag --all-targets -j 1 -- -D warnings` clean.

Spec / plan
- design: docs/superpowers/specs/2026-05-25-p9-fb-41-multi-hop-rag-design.md
- plan: docs/superpowers/plans/2026-05-25-p9-fb-41-multi-hop-rag.md (PR-3b 단락)

다음 단계 = PR-4 (CLI --multi-hop + wire schema + error_wire).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:17:37 +00:00
7150c376bb feat(rag): fb-41 PR-3a — HopRecord wire + RagCfg multi-hop knobs
PR-3 의 분할 첫 PR. wire additive (HopRecord + HopKind + Answer.hops
field) + RagCfg 의 multi_hop_* 3 노브. RAG pipeline 동작 미변경 —
모든 Answer literal 의 `hops = None`. PR-3b (후속) 가 ask_multi_hop
의 happy path 에서 dynamic decide loop 구현 + hops trace 채움.

분할 이유: 원래 PR-3 가 wire + cfg + decide loop + ScriptedLm +
helper refactor + 5+ tests 단일 PR 였는데 ~1500 줄 단일 patch 가
review 부담 + 회기 위험 ↑. additive foundation 부터 ship 후 decide
loop 별 PR — 사용자 결정 (2026-05-25).

- `kebab_core::HopRecord` (iter, kind, sub_queries,
  context_chunks_added, forced_stop, llm_call_ms) + `HopKind`
  (Decompose / Decide / Synthesize) — wire-additive shape.
- `kebab_core::Answer.hops: Option<Vec<HopRecord>>` —
  `#[serde(default, skip_serializing_if = "Option::is_none")]`,
  single-pass / refusal path 는 None, PR-3b 의 multi-hop happy
  path 가 Some.
- `kebab_config::RagCfg` 에 3 신규 노브:
  - `multi_hop_max_depth: u32` (default 3)
  - `multi_hop_max_sub_queries_per_iter: u32` (default 5)
  - `multi_hop_max_pool_chunks: u32` (default 30)
  3 모두 `#[serde(default)]` + env override
  (`KEBAB_RAG_MULTI_HOP_MAX_*`) + legacy parse 핀
  (`LEGACY_PRE_TIMEOUT_TOML` 공유).
- 9 Answer literal site (pipeline.rs ×6 + kebab-cli + kebab-tui
  tests + kebab-eval test) 에 `hops: None` 명시 추가. exhaustive
  field check 가 자동 guard — 빠진 site 시 compile fail.
- plan 의 PR-3 단락 → PR-3a / PR-3b 분할 명시 + scope 정정.

Tests (163 passing across kebab-config + kebab-core + kebab-rag):
- 5 신규 multi-hop knob test (default / env override / legacy parse).
- 기존 50+57+31+19+3+3 test 모두 hops:None 추가 후도 통과.

Wire 영향: `answer.v1` 의 optional `hops` 필드 — `skip_serializing_
if = None` 이라 single-pass response 에 emit 안 됨. wire breaking
아님, JSON Schema 갱신은 PR-3b 또는 PR-4 (실제 emit 시점).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 07:15:01 +00:00
cf35f36f88 feat(rag): fb-41 PR-2 — RagPipeline::ask_multi_hop skeleton (fixed depth=2)
PR-2 of fb-41 multi-hop RAG. Decompose + retrieve + synthesize 3-stage
pipeline가 `opts.multi_hop=true` 일 때 dispatch. Dynamic decide loop
는 PR-3.

- `AskOpts.multi_hop: bool` 필드 추가 + `impl Default for AskOpts`
  도입 (HOTFIXES 2026-05-07 의 known limitation 해소). 9 explicit
  init site 모두 `multi_hop: false` 추가 — Default 도입으로 향후
  `..Default::default()` 점진 migrate 가능.
- `RagPipeline::ask` 의 entry 에 dispatcher 한 줄
  (`if opts.multi_hop { return self.ask_multi_hop(...) }`).
- `RagPipeline::ask_multi_hop` 신규 method. 1) decompose LLM call
  → JSON array of strings parse, 2) 각 sub-query 로 retrieve +
  chunk_id dedup pool, 3) score gate / no-chunks 가드, 4)
  pack_context (single-pass 와 helper 공유), 5) synthesize LLM
  call w/ MULTI_HOP_SYNTHESIZE_SYSTEM_PROMPT, 6) citation extract
  + Answer build. `prompt_template_version` = "rag-multi-hop-v1"
  로 stamp — eval `compare` 가 single-pass vs multi-hop 분리.
- Prompt const 신규: MULTI_HOP_DECOMPOSE_SYSTEM_PROMPT +
  MULTI_HOP_DECOMPOSE_USER_TEMPLATE + MULTI_HOP_SYNTHESIZE_SYSTEM_PROMPT
  + PROMPT_TEMPLATE_VERSION_MULTI_HOP + MULTI_HOP_MAX_SUB_QUERIES_DEFAULT.
- `kebab_core::RefusalReason::MultiHopDecomposeFailed` variant 신규.
  Cascade: kebab-store-sqlite `refusal_reason_label` + kebab-tui `ask
  refusal render` exhaustive match 갱신.
- `parse_decompose_response` + `strip_markdown_json_fence` helper —
  markdown code fence (```json / ```) strip + JSON array of strings
  parse + trim + drop empty + cap at MULTI_HOP_MAX_SUB_QUERIES_DEFAULT.
  None 반환 시 caller 가 `MultiHopDecomposeFailed` refusal.

Tests (55 passing total, 8 신규):
- 6 unit (parse_decompose_response 의 bare array / fence variants /
  garbage / cap / trim 회귀 핀).
- 2 integration: `ask_multi_hop_dispatches_and_decompose_garbage_refuses`
  (decompose garbage → MultiHopDecomposeFailed + 정확히 1 LLM call) +
  `ask_with_multi_hop_false_keeps_single_pass_path` (회귀 핀, 기존
  caller 자동 backwards-compat).

Happy-path multi-hop (decompose 성공 → synthesize) 의 integration
test 는 ScriptedLm helper 가 PR-3 의 decide loop 와 함께 도입될
때 같이 추가. 현 `MockLanguageModel` 는 canned single response 라
2-LLM-call sequence 핀 불가.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:45:32 +00:00
453ec15df4 fix(dogfood): document-centric fetch_span + remove get_asset_by_workspace_path
assets.workspace_path is INTENTIONALLY 'last-registered path' for twin
files (identical content at different paths share one asset row PK'd by
blake3 content hash). PR #146 made try_skip_unchanged document-centric;
PR #149 made reset --orphans-only document-centric; this PR removes the
last caller of get_asset_by_workspace_path (fetch.rs:193 in fetch_span,
which used it to reject PDF/audio media — for twins this could read the
wrong asset's media_type and pick the wrong branch).

Replaced with the natural 2-step lookup: get_document_by_workspace_path
(PR #146) → doc.source_asset_id → get_asset (NEW trait method, asset_id
is PRIMARY KEY so flip-flop-immune by construction).

Then removed get_asset_by_workspace_path trait method + SqliteStore impl
— 0 callers after the refactor.

UPSERT doc-comment refreshed in store.rs to make the 'last-registered'
semantics explicit so future readers don't try to 'fix' the flip-flop.

Dogfood follow-up (PR #142 1B + multi-root corpus).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 08:03:38 +00:00
27baec82ea fix(dogfood): auto-purge stored docs for filesystem-deleted files
Files deleted from disk (rm a.md) were leaving stale documents + chunks +
embeddings in the store, surfacing as ghost citations in search/ask.
Existing purge_orphan_at_workspace_path only handled content-changed
stale (WHERE workspace_path=? AND asset_id != ?) — file deletion has no
new asset_id.

Fix: post-walker-scan sweep. Compute (stored_paths - scanned_paths),
for each candidate check filesystem existence — only purge when the
file is TRULY missing. Scope-narrowing case (file on disk but outside
include glob) is explicitly NOT purged to protect users from accidental
data loss via config edits.

Adds:
- DocumentStore::all_workspace_paths trait method + SqliteStore impl
- purge_deleted_workspace_path in store-sqlite (returns chunk_ids for
  vector delete; deletes doc CASCADE + asset row + copied storage file)
- sweep_deleted_files in kebab-app::ingest path; called once per ingest
  before the per-asset loop
- IngestReport.purged_deleted_files counter (additive, serde default)
- CLI ingest summary mentions purge count when > 0
- 2 integration tests: file_deletion_auto_purge + include_scope_narrowing_does_NOT_purge

dogfood discovery (PR #142 1B + multi-root: kebab-docs + httpx + zod
+ lodash). Per user decision: only filesystem deletion auto-purges;
scope narrowing requires explicit kebab reset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:51:07 +00:00
641b92af7d fix(dogfood): document-centric try_skip_unchanged for twin-file idempotency
Identical-content files at different workspace paths share one assets row
(assets.asset_id = blake3 content hash, PRIMARY KEY). The UPSERT
`ON CONFLICT(asset_id) DO UPDATE SET workspace_path = excluded` made
twin files overwrite each other's workspace_path on every ingest, so
`get_asset_by_workspace_path(path1)` returned the OTHER twin's row (or
None) — break idempotent unchanged-detection for both files.

Fix: switch try_skip_unchanged to document-centric lookup. `documents.
workspace_path` is already UNIQUE (V001) and `id_for_doc(path, ...)`
includes path, so each twin has its own stable document row. Compare
`doc.source_asset_id` with the new asset's checksum instead of going
through the assets table.

Dogfood (multi-root: kebab-docs + httpx + zod + lodash) showed 27 of
726 docs marked Updated on every idempotent re-ingest — all 27 are
twin-file victims (empty `__init__.py` ×3, AGENTS.md ↔ CLAUDE.md
same content, duplicate logo PDFs/JPGs).

After: re-ingest reports 0 new / 0 updated / 726 unchanged.

No schema migration needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:27:21 +00:00
7a6a24ad10 feat(p10-1a-2): add MediaType::Code(lang) variant
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>
2026-05-19 17:14:45 +00:00
9f3edb7e24 feat(p10-1a-2): add internal SourceSpan::Code variant + design §3.4 sync
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:52:01 +00:00
th-kim0823
bf4ebf8d2a feat(p10-1a-1): add Metadata.repo / git_branch / git_commit / code_lang
Four optional, serde-skipped-when-None fields added to `Metadata` for
code ingest context. All 11 downstream construction sites patched with
`repo: None, git_branch: None, git_commit: None, code_lang: None`.
Full workspace check (`--tests`) and per-crate test suite pass clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:44:18 +09:00
th-kim0823
351c7a0826 feat(p10-1a-1): add IngestReport skip counters + SkipExamples
Adds five new u32 counters (skipped_gitignore, skipped_kebabignore,
skipped_builtin_blacklist, skipped_generated, skipped_size_exceeded)
and a SkipExamples struct (≤5 sample paths per category) to
IngestReport. All new fields are #[serde(default)] for backward-compat
deserialization. Downstream literal construction sites patched with
zeros/empty; snapshot re-baked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:28:19 +09:00
th-kim0823
fa4eeb5a87 feat(p10-1a-1): add SearchHit.repo / code_lang + SearchFilters.repo / code_lang
Wire two new optional fields onto SearchHit (skip_serializing_if = None)
and two Vec<String> filter fields onto SearchFilters (serde default).
Add RetrievalDetail::Default impl (manual, uses SearchMode::Hybrid as
sentinel). Patch all downstream SearchHit / SearchFilters literal
constructors with repo: None / code_lang: None / vec![] as appropriate.
Also covers Citation::Code arm in kebab-eval metrics match.
2026-05-15 15:04:23 +09:00
th-kim0823
3b1e878aed feat(p10-1a-1): add Citation::Code variant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:39:18 +09:00
th-kim0823
892175d009 feat(core): BulkSearchItem / Summary / Response types (fb-42) 2026-05-10 20:12:31 +09:00
th-kim0823
3c605b1a5d feat(core): ScoreKind enum + SearchHit.score_kind (fb-38) 2026-05-10 17:49:02 +09:00
th-kim0823
6a33d08aea fix(fb-37): address PR #129 round 1 review
- doc TraceFusionInput.fusion_score semantics (single-mode vs hybrid)
- comment why total_ms vs stage sum can drift (millis truncation)
- TODO marker on TUI trace popup filter passthrough

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:26:34 +09:00
th-kim0823
69c6e23432 feat(store): breakdowns + index_bytes helpers (fb-37)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 12:24:43 +09:00
th-kim0823
1e943f21dc feat(core): SearchTrace + IndexBytes types + SearchOpts.trace (fb-37)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 12:17:04 +09:00
th-kim0823
d3f38c76e9 feat(core): SearchFilters gains media / ingested_after / doc_id (fb-36)
3 additive optional fields. #[serde(default)] preserves
backwards compat for older JSON without the new keys.
MEDIA_KINDS const exposes canonical "markdown"/"pdf"/"image"/
"audio"/"other" labels for downstream alias normalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:36:45 +09:00
th-kim0823
9653592c16 feat(core): FetchQuery / FetchOpts / FetchResult / FetchKind (fb-35)
Domain types for `kebab fetch` 3 modes (chunk / doc / span). All
types Serialize so wire layers hand them through serde_json
directly. FetchKind is snake_case-renamed to match the wire
discriminator literal in fetch_result.v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:35:21 +09:00
th-kim0823
e00418537f feat(core): SearchOpts domain type for budget controls (fb-34)
3 optional knobs (max_tokens, snippet_chars, cursor); Default = all
None = no enforcement (backwards-compat existing search behavior).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 17:46:40 +09:00
th-kim0823
307fd8d527 feat(rag): pipeline emits StreamEvent + cancel on SendError (fb-33)
RetrievalDone after retrieve+stale-stamp, Token per LM chunk
(SendError → break, FinishReason::Cancelled, RefusalReason::
LlmStreamAborted), Final on success. answers row still persists
on cancel for audit. Adds FinishReason::Cancelled, re-exports
StreamEvent from kebab_rag, migrates two pre-fb-33 sink tests
in tests/pipeline.rs to the new StreamEvent type (the
"dropped receiver does not abort" test inverts to record cancel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:49:55 +09:00
th-kim0823
3a7a28e682 feat(core): AnswerCitation gains indexed_at + stale (fb-32)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:56:36 +09:00
th-kim0823
8b0f64db6b feat(core): SearchHit gains indexed_at + stale (fb-32)
Domain field additions for p9-fb-32. Wire serialization is
automatic via serde rfc3339. Other crates fail to compile until
they populate the new fields — fixed in subsequent tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:52:46 +09:00
693f5582f0 feat(kebab-core, kebab-app): p9-fb-25 task 4 — IngestReport.skipped_by_extension + wire schema additive
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:06:34 +00:00
366e89e5e2 feat(kebab-store-sqlite): p9-fb-23 task 4 — get_asset_by_workspace_path
Add `DocumentStore::get_asset_by_workspace_path` trait method to
`kebab-core` and implement it on `SqliteStore` via a private
`asset_from_row` helper. Used by the incremental-ingest skip path to
compare a freshly-computed blake3 checksum against the persisted row
without a full round-trip through `put_asset_with_bytes`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:58:23 +00:00
f867b36afb feat(kebab-core): p9-fb-23 task 2 — CanonicalDocument gains last_chunker_version + last_embedding_version
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:50:25 +00:00
aa2a6ea7fc feat(kebab-core): p9-fb-23 task 1 — IngestItemKind::Unchanged + IngestReport.unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 17:43:52 +00:00
c97e8e00ef feat(kebab-core + kebab-store-sqlite): p9-fb-17 chat session storage (V005)
도그푸딩 item 13/14 (multi-turn 영속화) — TUI Ask 의 "이전 대화
이어가기" + 향후 CLI `--session foo` (p9-fb-18) backing store. session
header + per-turn 두 테이블, ON DELETE CASCADE 로 reset --data-only 가
한꺼번에 wipe.

## 핵심 변경

- **SQLite V005 migration** `chat_sessions` (session_id PK + created_at
  + updated_at + title + config_snapshot_json) + `chat_turns` (turn_id
  PK + session_id FK ON DELETE CASCADE + turn_index + question +
  answer + citations_json + created_at + UNIQUE(session_id, turn_index))
  + `idx_chat_turns_session(session_id, turn_index)`. 모두 `STRICT`.
- **`kebab_core::ChatSessionRepo`** trait (6 method): create_session /
  get_session / list_sessions(limit, ORDER BY updated_at DESC) /
  delete_session / append_turn / list_turns(ORDER BY turn_index ASC)
- **`kebab_core::{ChatSessionRow, ChatTurnRow}`** structs — Serialize
  + Deserialize 둘 다 (CLI / wire 출력 호환)
- **`kebab-store-sqlite::SqliteStore`** impl 신규 모듈 `chat_sessions.rs`.
  `append_turn` 이 insert + parent updated_at bump 같은 connection
  에서 처리.
- **frozen design §5** 에 §5.7a chat_sessions / chat_turns 절 신설
  (full schema + trait 메서드 6 개 명시).

## HOTFIXES (V004 → V005)

spec p9-fb-17 의 `V004__chat_sessions.sql` 가 p9-fb-19 의
`V004__kv.sql` (이미 머지) 와 refinery migration number 충돌. 무중단
정정: `V005__chat_sessions.sql` 로 시프트. schema / 동작 동일, 파일명
만 이동. HOTFIXES entry 추가.

## 테스트

- 9 신규 integration unit (create/get roundtrip, missing→None, PK
  collision error, append+list ordered, dup turn_index error,
  append bumps updated_at, delete CASCADE turns, list_sessions
  ORDER BY updated_at DESC, list_sessions LIMIT)
- workspace 전체 `cargo test --workspace --no-fail-fast -j 1` exit 0
- `cargo clippy --workspace --all-targets -- -D warnings` clean

## 문서

- frozen design §5.7a 신설
- HANDOFF: 2026-05-03 entry
- HOTFIXES: V004 → V005 rename rationale
- spec status planned → in_progress

## Out of scope

- session 검색 / 필터 UI (p9-fb-18 의 `kebab ask --session list`
  같은 admin command 가 후속)
- 다른 store backend (postgres 등) — trait 만 정의, impl 은 SQLite

unblocks p9-fb-18 (CLI session/repl).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 05:37:53 +00:00
2c058ab175 feat(rag): multi-turn ask — Turn struct + ask_with_history + token budget (p9-fb-15)
Spec PR #59 의 §3.8 multi-turn behaviour 구현. RAG facade 가 prior
turns 받아 prompt 에 prepend, retrieval query expansion 적용,
Answer 에 conversation_id / turn_index 채움.

신규 (kebab-core):
- Answer 에 conversation_id (Option<String>) / turn_index (Option<u32>)
  field 추가. serde skip_serializing_if 로 single-shot 의 wire
  output 변경 0 (기존 외부 wrapper 영향 없음).
- Turn struct (question + answer + citations + created_at).
- RefusalReason::LlmStreamAborted variant.

신규 (kebab-rag):
- AskOpts 에 history (Vec<Turn>) / conversation_id / turn_index 3 field.
- AskOpts::single_shot(mode) helper.
- RagPipeline::ask_with_history(query, history, conversation_id,
  turn_index, opts) — combined opts 로 ask 호출.
- expand_query_with_history: history.last() 의 answer 첫 200 자
  concat 해 SearchQuery.text 확장 (spec §3.8 의 \"cheap concat\";
  LLM-based standalone-question rewriting 은 P+).
- serialize_history + remaining_history_budget_chars: spec 의 priority
  enforcement — system+question 필수, retrieved chunks 가 차지한
  뒤 남은 char budget 안에서 newest 우선, oldest drop.
- ask 본문: history 가 비어있지 않으면 [이전 대화] 블록을 user
  prompt 위에 prepend. Answer 생성 site 3 곳 (정상 / NoChunks /
  ScoreGate refuse) 모두 conversation_id / turn_index 채움.

신규 (kebab-store-sqlite):
- refusal_reason_label 가 LlmStreamAborted → 'llm_stream_aborted'.

기존 caller 변경 (single-shot 동작 동일):
- kebab-cli main.rs Cmd::Ask: AskOpts 에 history=Vec::new(),
  conversation_id=None, turn_index=None 명시 (CLI multi-turn 은
  p9-fb-18 의 --session/--repl 가 채움).
- kebab-tui src/ask.rs spawn site 동일 (multi-turn UI 는 p9-fb-16).
- kebab-eval runner.rs golden eval 동일 (single-shot per query).
- kebab-app tests/ask_smoke.rs / kebab-tui tests/ask.rs / kebab-rag
  tests/pipeline.rs / kebab-eval metrics.rs Answer literal 갱신.

Test:
- 9 신규 lib unit (expand_query 4 / serialize_history 3 / remaining_budget 2).
- 기존 12 PASS 회귀 0.

Plan 갱신:
- p9-fb-15 status planned → in_progress. 머지 후 한 줄 commit
  으로 completed flip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 23:09:46 +00:00
0c8821f857 fix(kebab-store-vector): close P7-3 vector orphan caveat — delete_by_chunk_ids
P7-3 의 storage UNIQUE bug fix 가 SQLite 측 (documents → blocks /
chunks / embedding_records) 만 sweep 했음. LanceDB 의 vector 는 별도
store 라 옛 chunk_id 를 가진 row 가 디스크에 잔존. 검색에는 영향 없지만
디스크는 무한 누적. HOTFIXES `2026-05-02 P7-3` caveat 의 "P+ task" 약속을
같은 후속 PR 안에서 닫음.

변경:
- `VectorStore::delete_by_chunk_ids(&[ChunkId])` trait method 추가 (default
  no-op 제공 — 테스트 fake / 기존 impl 이 그대로 컴파일).
- `LanceVectorStore::delete_by_chunk_ids` 가 connection 의 모든
  `chunk_embeddings_*` 테이블을 순회 + `Table::delete("chunk_id IN (...)")`
  를 batch=200 단위로 실행. 다중 모델 워크스페이스 (마이그레이션 중간 등)
  에서도 안전.
- `SqliteStore::stale_chunk_ids_at(workspace_path, new_asset_id)` 가
  read-only SELECT 로 옛 chunk_id 들 반환. CASCADE 가 흐르기 *전* 에
  caller 가 호출.
- `kebab-app::purge_vector_orphans_for_workspace_path` 가 위 두 단계를
  orchestrate. 세 ingest path (markdown / image / pdf) 의
  `put_asset_with_bytes` 호출 직전에 한 줄로 호출.

Smoke 검증 (release binary, fastembed enabled):
- whitepaper.pdf 첫 ingest → chunk_ids = {f616…, 4e0f…}, vector store 에
  그 두 ID 의 row 존재.
- byte 변경 후 re-ingest → 새 doc_id (3741…) + 새 chunk_ids
  (ed0c…, e13c…). vector search "REWRITTEN chapter two" → 새 chunk_ids 만
  hit. 옛 query "Edited page two body" 시도해도 옛 chunk_ids 는 vector
  store 에 더 이상 없음 (의미적으로 가장 가까운 새 chunks 가 hit).

HOTFIXES `2026-05-02 P7-3` 의 \"vector store cleanup\" 항목이 \"deferred\" →
\"closed by follow-up PR\" 로 갱신. SMOKE.md 의 알려진 동작 (\"옛 vector
잔존\") 도 \"두 store 정합\" 으로 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 12:32:29 +00:00