Commit Graph

78 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
74e28293ba refactor(app): #231 derivation cache 배칭 + 계측 — 가설은 재현 안 됨
#231 은 "캐시가 히트하는데도 우회하고 전량 재임베딩하는 편이 더 빠르다" 고
보고하면서 본문에 "⚠️ 정량 실측 보완 필요 … 수치 미기록" 이라고 표를 비워
뒀다. 그 표를 채우는 것이 이 커밋의 핵심이다.

측정 (나무위키 792문서 / 16,379 chunk, ollama arctic-embed2 1024-dim,
--force-reingest 로 전 문서 full re-process):

  캐시 히트 경로              139.1초   236 chunk/초
  캐시 우회(전량 재임베딩)   1179.6초    28 chunk/초

캐시 히트가 8.5배 빠르다. 가설은 이 환경에서 재현되지 않는다.

새로 넣은 계측으로 139초의 내역을 보면 더 분명하다 (히트 32,758 / 미스 0):

  Lance upsert + 레코드 구성   74.6초  53%
  SQLite 문서·청크 기록        56.7초  41%
  chunk                         4.4초   3%
  캐시 경로 전체(조회+삽입+touch) 0.6초   0.4%

이슈가 지목한 여섯 원인이 전부 합쳐 run 의 0.4% 다.

다만 "보고가 틀렸다" 로 읽으면 안 된다. 보고 이후 #229#230 이 머지됐고,
#231 본문 스스로 #229 를 "같은 Mutex<Connection> 을 공유하므로 상호 증폭"
이라고 적었다. 원 보고 환경에서는 캐시 조회 52,000회가 그 뮤텍스를 잡았다
놓는데 같은 뮤텍스 위에서 chunk 삭제가 FTS5 전체 스캔을 돌리고 있었다.
"#229 를 먼저 고치면 체감이 줄어든다" 도 이슈의 예측이다. 이 머신이 61 GB
RAM 이라 DB 가 통째로 페이지 캐시에 올라간다는 점도 함께 적어 둔다.

반영한 것:

제안 1·2·4 는 실측과 무관하게 왕복이 줄 뿐 잃는 게 없어 넣었다. 다만 A/B
벽시계는 139.1초 → 139.3초로 측정 오차 안이다. 이 코퍼스에서는 체감이 없다.

  - derivation_cache_get_many — `WHERE cache_key IN (…)` 배치 조회.
    문서 하나가 평균 21 chunk 이라 왕복이 21회에서 1회가 된다.
  - derivation_cache_put_many — 미스 벡터를 한 트랜잭션에. 기존 단건 put 은
    명시 트랜잭션 밖이라 행마다 암묵 커밋이었다.
  - prepare_cached — get/put/touch 셋 다. query_row 는 호출마다 SQL 을
    다시 파싱한다.

제안 5(계측 노출)가 실질 산출물이다. `asset_timings` 에 cache_hit /
cache_miss / cache_ms 를 additive 로 실었다. 이전에는 hit/miss 가
tracing::info! 로 stderr 에만 나가 run 이 끝나면 사라졌고, "내 코퍼스에서
캐시가 이득인가" 를 확인할 방법이 없었다. cache_ms 에는 touch 도 포함한다 —
이슈의 가장 날카로운 지적이 "읽기 전용이어야 할 히트 경로가 쓰기를 만든다"
인데, touch 를 빼고 재는 지표로는 그 주장을 검증할 수 없다.

네 out-param 은 CacheStats 구조체로 묶었다. 함께 읽히고 함께 보고되는
값들이고, 셋만 갱신하고 하나를 빠뜨리면 캐시가 공짜인 것처럼 보고된다.

반영하지 않은 것:

  - 제안 3(touch 를 히트 경로에서 분리). 캐시 경로 전체가 0.6초라 touch 만
    떼어낼 이유가 없고, 권한 (c)안은 LRU 를 age 기반 축출로 바꾸는 의미
    변경이다. 근거 없이 할 변경이 아니다.
  - 제안 6(캐시 우회 스위치). 이슈 스스로 "1~4 로 해결되면 불필요 —
    플래그부터 만들지 말 것" 이라고 적었다.
  - 원인 6(4 KB BLOB overflow). page_size 변경은 기존 DB 에서 VACUUM 을
    요구하는데 kebab 은 VACUUM 을 실행하지 않는다. 0.4% 에 낼 비용이 아니다.

곁다리로 #228 에서 내가 넣은 flaky test 를 고쳤다.
`ingest_log_records_the_deleted_file_sweep` 이 두 run 의 로그 중 뒤엣것을
파일명 정렬로 골랐는데, run id 가 `<초 단위 타임스탬프>-<난수 hex>` 라 같은
초에 끝난 두 run 은 난수 쪽으로 정렬된다. 이번 전체 테스트에서 우연히 터져
잡았다. 첫 run 의 로그 집합을 기록해 두고 차집합으로 고르도록 바꿨다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 23:39:07 +09:00
8dbfa89b57 chore: PR #235 회차 2 리뷰 반영 — doctor 점검의 오탐·부작용 제거
2회차 리뷰가 1회차 지적 6건은 반영됐다고 확인했고, 대신 **1회차 대응으로
새로 넣은 `fts_shadow` 점검 자체에** HIGH 1건 + MEDIUM 2건을 찾았다.

1) 진단 명령이 없던 스토어를 만들었다 (HIGH)

   `SqliteStore::open` 은 마이그레이션은 안 돌리지만 `Connection::open` 이
   파일을 만든다. KB 없는 머신에서 `kebab doctor` 한 번이
   `<data_dir>/kebab.sqlite` 를 남겼고, 그러면 이후 `open_existing` 이
   성공해 버려 `not_indexed` 로 갈렸어야 할 경로가 일반 오류로 바뀐다.
   `--readonly` 규약도 진단 명령이 깬다.

   `SQLITE_FILE` 을 공개하고 파일 존재를 먼저 확인한 뒤에만 연다.
   `doctor_does_not_create_a_store_where_none_exists` 로 고정.

2) V016 미적용 스토어를 고장 났다고 하고, 파괴적 조치를 권했다 (MEDIUM)

   doctor 는 마이그레이션을 돌리지 않으므로 V015 스토어를 그대로 읽는다.
   그런데 V009 백필이 rowid 를 명시하지 않아서, 그 시점 `chunks.rowid` 에
   구멍이 있던 스토어는 V015 에서 **이미 어긋나 있다**. 거기서는 삭제가
   `chunk_id` 기준이라 무해한데, 새 점검이 `ok: false` → exit 3 + "`kebab
   reset` 후 재색인" 을 냈다. 바이너리를 올리고 doctor 부터 돌리는 자연스러운
   순서에서 멀쩡한 KB 를 날리라고 안내한 셈이다.

   `migration_version()` 을 보고 V016 미만이면 `ok: true` + "마이그레이션
   후 점검된다"로 간다. 실제 해법이 그것이다 — V016 의 명시 rowid
   repopulate 가 이 어긋남을 고쳐 준다.
   `doctor_does_not_call_a_pre_v016_store_broken` 으로 고정.

3) env override 를 무시해 엉뚱한 DB 를 검사했다 (MEDIUM)

   바로 위 `data_dir_writable` 은 "Config::load 와 같은 precedence 유지"를
   이유로 env 를 다시 얹는데 이 블록은 안 했다. `KEBAB_STORAGE_DATA_DIR` 을
   쓰면 data_dir 은 A 로 보고하면서 인덱스는 B 를 검사한다. 같은 방식으로
   맞췄다.

4) 잔가지 (LOW)

   - `None` 분기가 "스토어 없음 / 열기 실패 / 읽기 실패"를 전부 "KB 없음"
     으로 뭉갰다. 조용한 실패를 드러내려는 점검이 자기 실패를 삼키면 안
     되므로 "점검하지 못했다" 를 따로 뒀다 (hint 가 있으므로 CLI 가 `!`
     로 찍는다).
   - "표본 400행" 이 chunk 400개 미만인 스토어에서 거짓이었다. ASC/DESC
     두 창이 겹치면 분자와 분모를 둘 다 두 번 셌다. UNION 으로 바꾸고
     실제 표본 수를 함께 돌려준다 — 반환형이 `(checked, misaligned)` 다.
   - `pub const SQLITE_FILE` 위에 "Kept private" 이라는 옛 독 주석이
     남아 있었다.
   - README 의 doctor 행에 `fts_shadow` 가 exit 3 을 낼 수 있다고 적었다.
     새 플래그도 config 키도 없지만 **doctor 가 실패하는 새 사유**는
     스크립트와 에이전트가 분기하는 사용자 표면이다.
   - `tasks/phase-2-lexical-search.md` 가 `kebab index --rebuild-fts` 를
     산출물로 나열하고 있었다. 배선된 적 없는 명령이고 이 PR 이 "CLI
     경로 없음"이라고 못박은 것과 어긋나서 정정했다.

실측 확인: env override 를 준 doctor 가 지정한 KB 를 검사하고("양끝 400행
표본"), V015 실제 KB(28,427 문서)는 "V016 적용 전 (현재 V015)" 로 나오며,
KB 없는 경로에서는 파일을 남기지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 20:00:22 +09:00
17167e6943 chore: PR #235 회차 1 리뷰 반영 — 실패 양상 탐지 + 사실관계 정정
리뷰 두 건에서 나온 지적을 반영한다.

1) 실패 양상이 바뀐 것을 다루지 않았다 (MEDIUM)

   `chunk_id` 로 행을 찾던 때는 shadow 정렬이 어긋나도 느릴 뿐 정확했다.
   rowid 로 찾으면 어긋난 순간 `chunks_ad` 가 남의 문서 shadow 행을 지우고
   아무 오류도 내지 않는다. 즉 이 PR 은 실패 양상을 "느림"에서 "조용한
   오삭제"로 바꿨는데, 그 불변식이 눈에 안 보이는 상태였다.

   `kebab doctor` 에 `fts_shadow` 점검을 넣었다. 전수 대조는 60만 chunk
   에서 33초라 doctor 앞에 둘 수 없어 rowid 범위 앞뒤 200행씩만 본다 —
   실측 10 ms 이고, 현실적인 드리프트가 취하는 전면 재번호는 잡는다.
   표본이라는 사실을 detail 에 적어 정렬 증명으로 읽히지 않게 했다.
   `SqliteStore::fts_shadow_misaligned_sample` 이 질의를 들고 있다.

2) VACUUM 위험을 과장했다 (정정)

   초안이 "VACUUM 이 rowid 를 다시 매길 수 있고 그러면 정렬이 깨진다"고
   단정했다. 실제로 재보니 다시 매기지 않았다 — 실제 KB 사본(60만 chunk,
   문서 3,000건을 지워 rowid 에 구멍을 낸 뒤)과 소형 합성 DB 양쪽에서
   VACUUM 후 전수 대조 불일치가 0 이었다 (sqlite 3.53.4). SQLite 문서가
   "다시 매길 수 있다"고 적은 것은 보장이 없다는 뜻이지 실제로 그렇게
   한다는 뜻이 아니다. 문구를 실측대로 고쳤다.

   남는 실제 경로는 앞으로 `chunks` 를 테이블 재작성 방식으로 바꾸는
   마이그레이션이다. V016 주석에 "그런 마이그레이션은 repopulate 를 같이
   돌려야 한다"는 울타리를 박았다.

3) 같은 실측치를 파일마다 다르게 적었다 (MEDIUM)

   삭제 시간이 커밋 메시지·HOTFIXES 는 2.0초, 마이그레이션 주석·테스트
   독스트링·설계 문서는 0.73초였다. 0.73초는 손으로 마이그레이션한 사본을
   따뜻한 캐시에서 잰 값이고 2.0초는 릴리스 바이너리가 마이그레이션한 새
   사본에서 잰 값이다. 보수적인 2.0초로 통일했다. '한국' hit 수도
   15,837(문서 200건 삭제 후) 과 15,977(전체 코퍼스) 이 섞여 있어
   15,977 로 통일했다.

4) docs/ARCHITECTURE.md 디렉토리 트리가 V001..V015 로 멈춰 있었다 (MEDIUM)

   V016 까지로 갱신. README 는 손대지 않는다 — 새 서브커맨드·플래그·config
   키·`--json` 필드가 없다.

5) 잔가지 (LOW)

   `:=` 검사가 번들 SQLite 의 FTS5 idxStr 인코딩에 기대는 것을 assert
   메시지에 적었다 (rusqlite 를 올린 직후 실패하면 거기부터 보라는 뜻).
   가상 테이블은 항상 `SCAN` 으로 찍히므로 `SEARCH` 로 대체 검사할 방법이
   없다는 것도 독스트링에 남겼다. `kb index --rebuild-fts` 라는 옛 이름 +
   존재하지 않는 명령 참조 두 곳을 지웠다.

`fts_v016_shadow_probe_detects_forced_drift` 로 탐지 자체를 시험한다 —
어긋난 shadow 행을 억지로 만들어 점검이 잡는지 본다. 잡지 못하는 점검은
없느니만 못하다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 19:39:04 +09:00
70b7c01fce fix(store-sqlite): #229 chunks_fts 삭제를 전체 스캔에서 rowid 조회로 (V016)
`chunk_id` 는 `chunks_fts` 에서 UNINDEXED 다. 그런데 V002 이래 삭제
트리거가 그 컬럼으로 행을 찾았다 (`DELETE FROM chunks_fts WHERE
chunk_id = old.chunk_id`). FTS5 는 UNINDEXED 컬럼에 색인을 만들지 않으니
이 조건을 만족할 색인이 없고, 삭제가 색인 전체 스캔으로 떨어진다.
chunk 한 건 삭제가 O(색인 전체) 였다.

`chunks` 에서 DELETE 가 나가는 모든 경로가 이 비용을 냈다 —
sweep_deleted_files, reset --orphans-only, 그리고 파일이 수정될 때마다
도는 purge_orphan_at_workspace_path. 즉 정상적인 증분 재색인이 코퍼스가
커질수록 느려지는 형태였다.

V016 이 chunks_fts 를 drop 후 재생성하면서 rowid 를 chunks.rowid 와
맞추고, 세 트리거의 행 지정을 rowid 로 바꾼다. FTS5 는 rowid 로 B-tree
조회를 하므로 O(log n) 이 된다. 컬럼 구성·tokenizer 는 그대로라 검색
경로는 한 줄도 안 바뀐다.

실제 KB 사본 (문서 28,427건 / chunk 600,808건) 에서 문서 200건 삭제:

  현행 (chunk_id 로 DELETE)  1590.1초
  V016 (rowid 로 DELETE)         2.0초

약 800배. 삭제 후 남은 chunks 와 chunks_fts 행 수가 양쪽 다 595,741 로
같다. 마이그레이션 자체는 60만 chunk 기준 32초이고 재색인은 필요 없다.

검색 결과는 불변이다. '한국'(15,977) / 'database'(1,067) / '서울
지하철'(230) / 'kebab'(3) 네 질의의 상위 20건을 chunk_id·bm25
점수·snippet 까지 해시로 비교했고 전후가 동일했다. 그래서 corpus_revision
을 올리지 않는다 — 어휘 검색 정렬이 `ORDER BY score, f.chunk_id` 라
rowid 와 무관하므로 미결 pagination cursor 를 무효화할 이유가 없다.
tokenizer 가 바뀐 V009 와는 다른 경우다.

이슈가 제안한 external-content(`content='chunks'`) 는 택하지 않았다.
V009 트리거가 색인하는 값이 `tokenized_korean_text || ' ' || text` 라
chunks 의 어느 컬럼과도 일치하지 않아, generated column 신설 + FTS
테이블에서 chunk_id/doc_id 제거 + 검색 경로의 rowid join 전환이 딸려온다.
삭제 비용은 rowid 정렬만으로 같은 복잡도로 내려가므로 본문 그림자
(chunks_fts_content, 실측 550 MB) 회수는 별 건으로 남긴다.

rebuild_chunks_fts 도 같이 고쳤다. rowid 를 명시하지 않으면 FTS5 가 자기
번호를 매겨 정렬이 깨지고 그 뒤의 모든 삭제가 조용히 아무것도 안 하게
된다. 이 함수에는 별개의 잠복 결함도 있었다 — V009 가 색인하는 한국어
형태소 접두를 빠뜨리고 raw text 만 넣고 있어서, 재구축을 돌리면 2자
한국어 질의가 다음 재색인 때까지 안 맞았다. 트리거와 같은 CASE 로 맞췄다.

전제: chunks 는 chunk_id TEXT PRIMARY KEY 라 INTEGER PRIMARY KEY 가 없고,
SQLite 의 VACUUM 은 그런 테이블의 rowid 를 다시 매길 수 있다. kebab 은
VACUUM 을 실행하지 않으며(코드베이스 전체에 없음) 사용자가 직접 돌렸다면
rebuild_chunks_fts 가 복구 경로다. external-content 도 같은 전제를 깔고
있어 이 위험은 선택지 간 차이가 아니다.

design §5.5 verbatim block 을 rowid 트리거로 갱신하고 CI diff-check 를
V009 에서 V016 으로 재조준했다 (V007 → V009 때와 같은 방식).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 18:43:06 +09:00
8dfeb007e0 chore(search): PR #219 회차 1 리뷰 반영 — 잔존 dead-cache 주석/help text 정리
리뷰어 지적 6건 + 전체-트리 grep 으로 추가 발견한 동일 부류 4건:
- search --trace help text(CLI main.rs + MCP search tool) "Bypasses cache"
  → "always a fresh retriever run" (없는 캐시 광고 제거)
- corpus_revision 의 실제 소비자는 incremental ingest 가 아니라 cursor
  pagination(stale_cursor) 임을 반영 — HANDOFF 노트 사실 정정 + ingest.rs
  bump 주석/warn 로그 + corpus_revision/search_lexical 테스트 주석 재서술
  (검증: cursor.rs + app.rs:327/333/498 가 유일 소비자, ingest 는 bump 만)
- bulk.rs "LRU cache amortize" → embedder cold-start 만 amortize
- app.rs lexical_index_version "search cache 무효화" 제거
- app-facade README 의 search cache bullet 을 "(제거됨 — 이력)" 표시

잔여 dead-cache 개념 참조 0건(active tree), clippy -D warnings 클린.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-26 17:25:03 +00:00
2dfbbe4f43 refactor(config): consumer들이 &Config 대신 타입 슬라이스 수령 (god-struct 결합 해소) 2026-06-24 11:55:10 +00:00
1d32aa645a refactor: multi-turn 세션 제거 (ask --session, chat_sessions/turns) + V015 drop migration 2026-06-24 09:57:12 +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
e03d03cb26 test: 별칭 전용 테스트 삭제 + 영향 테스트/fixture 갱신
kebab-search/tests/lexical.rs 의 alias 채널 테스트 + insert_chunk_with_aliases
헬퍼 제거(body 회수 회귀 테스트로 대체). Chunk 리터럴 aliases: None 제거
(embedding_records_fk/idempotency/inspect). chunk 스냅샷 fixture 의 aliases
키 제거. config_migrate 는 ingest.code 앵커로, corpus_revision/search_lexical
주석은 V013 비-bump 명시로 갱신.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:37:58 +00:00
ecaf224381 refactor(chunk): Chunk 생성부의 aliases 리터럴 + store 컬럼 제거
kebab-chunk/* AST·md·tier2·pdf chunker 의 aliases: None 리터럴 삭제,
store-sqlite documents.rs chunks INSERT 컬럼/바인딩 + get_chunk 매핑에서
aliases 제거.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:36:44 +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
e9b520216e fix(expansion): per-alias sentinel orphan cleanup + 캐시 견고성 (PR #195 리뷰)
MAJOR: 별칭 dense 벡터의 chunk_id 가 레거시 단일 `{id}#alias` 에서 줄별
`{id}#alias#0`, `#alias#1`, … 로 바뀌었으나 orphan cleanup 이 단일 sentinel
하나만 삭제해 `#alias#N` 벡터가 LanceDB / embedding_records 에 누수됐다.

- kebab-app: `alias_sentinel_ids_to_delete` 헬퍼 추가(접근법 A) — 본문 +
  legacy `{id}#alias` + `{id}#alias#0`..`{id}#alias#{max-1}` 를 모두 delete-set
  에 포함. max=expansion.max_aliases_per_chunk(= parse_aliases 의 하드 cap)와
  일치. parser-bump / edited-asset / deleted-file 세 LanceDB cleanup 경로 모두
  이 헬퍼를 사용.
- kebab-store-sqlite: embedding_records 명시 DELETE 4 경로(put_chunks /
  purge_*_except_doc_id / purge_orphan_at_workspace_path /
  purge_deleted_workspace_path)를 정확 일치(`|| '#alias'`)에서 `{id}#alias%`
  프리픽스 LIKE 로 전환. 본문 chunk_id 는 32자 hex 라 LIKE 와일드카드 없음.

MINOR 1: alias 캐시 히트 시 비-UTF8 payload 를 미스로 강등(재생성 분기로)
— embedding 경로의 decode-실패→미스 강등과 동작 일치.
MINOR 2: embedding version_key 맨 앞에 kind 토큰("doc") 추가 — 임베더가
kind 별 프리픽스를 붙이므로 미래에 query 임베딩이 같은 캐시를 타도 충돌 방지.

회귀 테스트:
- kebab-app: alias_sentinel_ids_to_delete 단위 테스트 2건.
- kebab-store-sqlite: per-alias sentinel embedding_records 가 세 cleanup
  경로 모두에서 사라지는지 핀하는 통합 테스트 3건.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:14:34 +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
0282a81c67 fix(store): CASCADE 대체 4번째 경로 + V011 CHECK 복원 (Task 4.5 리뷰)
리뷰 MAJOR: purge_document_at_workspace_path_except_doc_id(parser-bump 경로)에
원본+sentinel embedding_records 명시 DELETE 누락 → tombstone 누적. 추가 +
회귀 테스트. MINOR: V011 status CHECK(pending/committed/tombstone) 복원.
NIT: foreign_keys PRAGMA no-op 주석.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 14:02:46 +00:00
f3587b7143 feat(store): filter_chunks sentinel 별칭 candidate strip (committed 통과)
LanceDB 후보의 sentinel chunk_id({orig}#alias)는 chunks JOIN 에서 탈락해
VectorRetriever strip 이전에 사라진다. candidate 를 kebab_core::strip_alias_suffix
로 원본 chunk_id 로 strip 해 IN-list/JOIN 에 넣어(committed 판정은 원본 body chunk
기준) 통과시키되, 반환은 입력 candidate 형태(sentinel 유지) — VectorRetriever 가
그 sentinel 을 받아 strip+dedup 한다. SQL replace 대신 (b) Rust strip 채택(명확).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:41:28 +00:00
483b1ec06b feat(store): V011 embedding_records FK 제거 + CASCADE 대체 명시 DELETE (sentinel 별칭 벡터)
별칭 dense 벡터를 sentinel chunk_id({orig}#alias)로 색인하려면 chunks 에 없는
chunk_id 가 embedding_records 에 들어가야 한다. V001 의 chunk_id REFERENCES chunks
ON DELETE CASCADE FK 가 이를 SQLite 787 로 막으므로 테이블을 FK 없이 재생성한다.
status/vector_committed(V003) + 3개 인덱스 보존, chunks_bd_tombstone_embeddings
trigger 무수정. DROP→RENAME 시 dangling trigger 재파싱을 피하려 legacy_alter_table=ON.

사라진 CASCADE 는 put_chunks + purge 두 경로(purge_orphan_at_workspace_path,
purge_deleted_workspace_path)의 명시 DELETE 로 대체 — chunks 삭제 직전 원본 +
{id}#alias sentinel embedding_records 를 함께 정리. corpus_revision baseline 2→3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:41:20 +00:00
0df47febf0 test(store): doc-side expansion Task 2 리뷰 보강 (M1/M2/N1)
- M1: chunk_aliases trigger 가드에 AND aliases <> '' (빈 문자열 미색인)
- M2: 재색인 멱등 테스트 (재-put 후 별칭 행 1개)
- N1: 본문 격리 음성 단언 (별칭 term 이 chunks_fts 로 누출 안 됨)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 02:24:24 +00:00
b12a616ab2 feat(store): V010 chunk_aliases_fts + put_chunks 별칭 영속화
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 02:15:27 +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
21b52bc285 style: cargo fmt --all (S3+S4+S5+S7 follow-up)
V009 morphological tokenizer 작업 (S3 chunk + S4 backfill + S5
short_query_hint 제거 + S7 신규 tests) 의 형식 정리. 동작 변경 없음.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S11)
2026-05-28 12:06:01 +00:00
c5de5f812b test(fts,app): V009 morphological tokenizer integration tests
신규 4 test 추가:

- crates/kebab-store-sqlite/tests/fts.rs:
  - fts_v009_korean_morphological_2char_query_hits: tokenized_korean_text
    column 이 채워진 chunk 의 '한국' 2-char query hit.
  - fts_v009_english_whole_token_only: V007 trigram substring 매칭
    회귀 (Path A) — 'token' query 가 'tokenizer' chunk 에서 0-hit.
- crates/kebab-app/tests/search_korean.rs:
  - korean_morphological_2char_query_lexical_mode: end-to-end
    한국어 wiki fixture ingest → '한국' / '서울' query hit.
  - korean_morphological_mixed_english_korean_query: 'Rust' English
    whole-token + '최적화' Korean morpheme hit.

crates/kebab-search/src/lexical.rs:
  - build_match_string() 의 MIN_TRIGRAM_CHARS(3) → MIN_QUERY_CHARS(2).
    V009 unicode61 은 최소 token 길이 제한 없어 2자 한국어 morpheme
    query 가 통과되어야 함. 1자 단독은 여전히 필터.
  - 관련 unit test 2개 V009 동작으로 갱신.

fixture text 는 lindera ko-dic 의 실제 segmentation 동작에 의존
(spec Appendix B prior-knowledge 예측). 실측 시 fixture 조정 가능.

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:38:52 +00:00
b63af20b72 feat(app): first-boot eager backfill for tokenized_korean_text
V007 → V009 업그레이드 시 기존 chunks 의 tokenized_korean_text 가
NULL — 첫 App::open_with_config 호출 시 자동으로 lindera ko-dic
으로 분해 후 UPDATE. chunks_au trigger 가 chunks_fts 를 자동 재-index.
사용자 재-ingest 불필요.

- crates/kebab-store-sqlite/src/store.rs:
  backfill_tokenized_korean_text(progress_cb, tokenize) API. 1000 row 마다
  commit + progress 콜백. idempotent (IS NULL 필터로 partial
  completion 재실행 안전). tokenizer 를 파라미터로 받아 §8 dep 경계 유지.
- crates/kebab-app/src/app.rs::open_with_config: run_migrations 직후
  backfill 호출. 실패 시 warn log 만 (App open 은 성공 — vector/hybrid
  mode 계속 가능). 500 row 마다 info log progress.
- crates/kebab-store-sqlite/tests/fts.rs:
  backfill_tokenized_korean_text_populates_nullable_rows 단위 test
  (idempotency 포함).
- clippy pre-existing 오류 수정 (redundant_closure, map_unwrap_or,
  cast_lossless, uninlined_format_args — kebab-app/ingest_log.rs,
  pdf_ocr_apply.rs, app.rs, tests/ocr_inspect_smoke.rs).

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 11:01:00 +00:00
4b4a8cbb3a fix(test): retire V007 trigram-only fts tests, add V009 unicode61 sanity
V007 trigram tokenizer 의 substring 매칭을 검증하던 3 test 는 V009
unicode61 으로 의도된 회귀 (spec §3 Non-Goals Path A) 가 발생하므로
obsolete:

- fts_trigram_korean_3char_substring_hits: '발생한' → '발생한다' hit
  은 trigram 의 substring 매칭이라 V009 의 whole-token 매칭에서 fail.
- fts_trigram_korean_short_query_zero_hit_pinned: 2-char Korean
  query 의 0-hit 동작은 V009 의 형태소 column 으로 해소되므로 이 핀
  자체가 obsolete (S7 이 신규 2-char hit test 로 대체).
- fts_trigram_english_substring_hits: 'token' → 'tokenizer' hit 은
  V009 unicode61 의 whole-token only 에서 fail.

신규 추가:
- fts_v009_unicode61_space_separated_korean_token_hits: V009 unicode61
  의 whole-token 매칭 sanity (token '충돌은' hit, substring '발생한'
  0-hit). S7 이 추가할 morphological 검증 test 와 별개의 baseline.

S7 (plan §2 Step 7) 가 v009_korean_morphological_2char_query_hits +
v009_english_whole_token_only 를 추가하여 회귀 + 신규 동작 모두 핀할
예정.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §3, §9.2
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S3 follow-up)
2026-05-28 10:36:32 +00:00
4dc1c10be1 fix(test): update corpus_revision baseline to post-V009 migration
V009 migration bumps corpus_revision by 1 at apply time (spec §5.2 —
invalidates pre-V009 LRU search cache). Existing tests assumed V004
seed (0) was the final baseline; updated to expect 1 after migration:

- fresh_store_starts_at_zero → fresh_store_starts_at_post_migration_baseline
- bump_increments_monotonically: expected 1,2,3 → 2,3,4 (post-baseline)
- revision_persists_across_reopen: 2 → 3 (V009 +1, +bump,+bump)

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §5.2
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S3 follow-up)
2026-05-28 10:33:50 +00:00
bd86f61c9c fix(chunk): close S3 reviewer blockers — get_chunk read + AST chunker cascade
S3 spec compliance reviewer (sonnet) 가 2 blocker 발견:

1. crates/kebab-store-sqlite/src/documents.rs: get_chunk SELECT 가
   tokenized_korean_text column 을 미조회 → DB 의 값이 read 시 유실.
   SELECT column list + row → Chunk 변환 시 row.get 인덱스 추가.
   ChunkRow struct + chunk_row_from_sql + get_chunk Chunk 생성 cascade.

2. crates/kebab-chunk/src/code_*_ast_v1.rs (9 file): make_chunk 가
   tokenized_korean_text: None 하드코딩 → 한국어 주석을 가진 코드
   파일이 FTS hit 안 됨. tier2_shared 와 동일 패턴으로
   tokenize_korean_morphological(text) 호출 cascade.

이 commit 은 S3 의 rework — amend 아닌 별 commit (S3 boundary
유지). spec §6.2 invariant ("모든 chunker 가 chunk emit 직전에
tokenize 호출") 충족.

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 rework)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 10:30:53 +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
b106120e93 feat(fts): add V009 korean morphological tokenizer migration
V007 trigram tokenizer 의 한국어 2자 query 0-hit 한계 (Bug #8) 해소를
위한 V009 migration 추가. unicode61 tokenizer 로 환원 + 한국어 형태소
분해 결과를 별 column `tokenized_korean_text` 에 pre-fill 하는 방식.

- migrations/V009__fts_korean_morphological.sql 신규: column ADD,
  chunks_fts DROP+재정의, 3 trigger CASE expression, backfill INSERT,
  corpus_revision bump.
- design §5.5 갱신: trigram → unicode61 + 형태소 column. CASE
  expression trigger 본문.
- crates/kebab-store-sqlite/tests/fts.rs: V007 verbatim test 를
  V009 source-of-truth 로 rename. v009_bumps_corpus_revision unit
  test 추가.
- store.rs: clippy bool_to_int_with_if + cast_lossless 기존 경고 수정
  (pdf_ocr_events 관련 코드, S1 작업 중 발견).

영어 substring 매칭은 V002 (whole-token only) 로 회귀 — spec §3
Non-Goals + 후속 release notes (v0.20.1) 에서 정직히 기술.

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 09:48:46 +00:00
9a36a06f97 style: cargo fmt --all (v0.20.x logging r2 feature follow-up)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 06:34:01 +00:00
6482bf1321 feat(store): V008 pdf_ocr_events migration + record/prune API (Enhancement 2)
Add migrations/V008__pdf_ocr_events.sql with the events table + 3
indices (doc_id, run_id, ts). SqliteStore gains two pub fn:
record_pdf_ocr_event (insert one OCR sample) and prune_pdf_ocr_events
(delete rows older than retention_days; returns the affected row
count). Both follow the existing Mutex<Connection> lock pattern.

Wiring into ingest path lands in the next commit.

Closure r1 F2: explicit lock acquisition in both methods.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 05:56:54 +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
d9c7aabce1 feat(schema): add active_parsers + active_chunkers arrays to schema.v1.models (Bug #13)
이전: schema.v1.models 가 parser_version / chunker_version 단일 값만 보고 →
multi-medium corpus (md + pdf + code Rust/Python + dockerfile + k8s + manifest)
의 version cascade audit 누락 risk.

이후: additive minor — Models struct 에 active_parsers + active_chunkers Vec<String>
추가. backward compat: 기존 단일 field 보존 (markdown default), 신규 array 는
optional (#[serde(default)] + JSON schema required 미포함).

source:
- kebab_store_sqlite::fetch_distinct_parser_versions() 가
  documents.parser_version DISTINCT + ORDER BY 반환.
- fetch_distinct_chunker_versions() 가 chunks.chunker_version 동일 pattern.
- collect_models 가 매 schema 호출마다 재계산 (cache 없음 — R-3 자동 해결).

wire schema additive only — 메이저 bump 불필요. v0.20.1 minor 로 충분.
integrations/claude-code/kebab/SKILL.md 동기 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:15:58 +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
710945c4b0 refactor(parse-md): absorb kebab-normalize + kebab-parse-types — 24 → 22 crates + §3.7b 재작성
design §3.7b 의 thin layer (ParsedBlock 류) 가 4 parser 중 1개 (markdown) 만 lift 를
경유하는 현실 — fan-in/fan-out 모두 1 → layer 의미 잃음. kebab-normalize (1097 LOC)
+ kebab-parse-types (98 LOC) 둘을 kebab-parse-md 로 흡수.

설계: docs/superpowers/specs/2026-05-26-normalize-absorption-spec.md
플랜: docs/superpowers/plans/2026-05-26-normalize-absorption-plan.md
HOTFIXES: tasks/HOTFIXES.md 의 2026-05-26 entry (design deviation)

- 5 사용 type + 3 forward-declared struct → kebab-parse-md::types module 의 pub explicit re-export.
- build_canonical_document + derive_title + warning_agent → kebab-parse-md::normalize module.
- 4 hard-coded agent literal (lib.rs:122/128/134/153) + warning_agent body return + tracing target literal 모두 보존 — stage label 일관성.
- kebab-app callsite (lib.rs:51 use + :1119 context string) + Cargo.toml 의 2 dep (regular + dead) 제거.
- kebab-chunk + kebab-store-sqlite 의 [dev-dependencies] kebab-normalize → 제거 (kebab-parse-md 로 갈음). 통합 test source 의 use shift.
- test file 이동 (kebab-normalize/tests/normalize_snapshot.rs → kebab-parse-md/tests/).
- workspace Cargo.toml: Hunk (a) members 2 entry 삭제 + Hunk (b) version 0.18.0 → 0.19.0 (frozen contract 변경).
- design §3.7b 4-단락 재작성 (원래 intent 보존 + 현재 상태 + 보존된 surface + future re-extraction trigger).
- design §8 graph 갱신 (3 edge 제거 + 2 forbidden bullet 의미 갱신 + commentary).
- ARCHITECTURE.md crate graph + directory tree mechanical 갱신.
- tasks/INDEX.md L169 closure mention + "Future work / deferred" 섹션 신설 (image/pdf normalize integration entry).
- tasks/HOTFIXES.md 신규 entry (4-block — design deviation Symptom).
- HANDOFF.md cross-link 한 줄.
- 3 dead struct (ParsedImageRegion / ParsedPdfPage / ParsedAudioSegment) 는 보존 — v0.20+ image/pdf normalize integration 의 future surface (spec §11).

Wire / surface impact: 0건. CLI / TUI / MCP / --json 출력 / config / XDG path /
parser_version 모두 unchanged. wire-invisible provenance.events[].agent + tracing target
literal "kb-normalize" 도 보존 — old DB row 와 new DB row 의 audit log 일관성.

Verification: cargo test --workspace --no-fail-fast -j 1 → 1313 passed / 0 failed (172 result blocks).
cargo clippy --workspace --all-targets -j 1 -- -D warnings → 0 warning (5m 46s).
cargo metadata --no-deps --format-version 1 | jq '.workspace_members | length' = 22.
cargo tree -p kebab-app --depth 2 | grep -E "kebab_(parse_types|normalize)" = 0 줄.
2026-05-26 15:00:59 +00:00
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
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
0def913abd feat(v0.17.0/PR-C): code_lang_chunk_breakdown additive wire field
closure of HOTFIXES 2026-05-22 "code_lang_breakdown chunk granularity"
LOW. Chunk-level companion of the existing doc-count metric.

- crates/kebab-store-sqlite/src/store.rs: code_lang_chunk_breakdown()
  method. chunks INNER JOIN documents → COUNT(c.chunk_id) GROUP BY
  metadata_json.code_lang, NULL skipped. BTreeMap<String, u32>.
  + lib unit test code_lang_chunk_breakdown_counts_chunks_not_docs
  (1 rust doc + 3 chunks → rust=3 chunks vs rust=1 doc).
- crates/kebab-app/src/schema.rs: Stats.code_lang_chunk_breakdown
  additive field + collect_stats builder. tests_stats_ext 의
  stats_includes_code_lang_and_repo_breakdown_fields 가 신규 필드도
  검증.
- docs/wire-schema/v1/schema.schema.json: 신규 additive 필드
  명세 + 기존 code_lang_breakdown / repo_breakdown description
  정정 ("code chunk count" → "doc count", Gemini round 2 권고).
- tasks/HOTFIXES.md: 2026-05-24 PR-C closure entry.

wire additive, schema_version bump 불필요. v0.16.x 호출 호환.
cargo test --workspace --no-fail-fast -j 1 + clippy 통과.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:35:01 +00:00
93ddece111 feat(v0.17.0/PR-B/B1): C typedef extractor + parser_version bump + orphan purge cascade
closure of HOTFIXES 2026-05-21. C typedef-wrapped anonymous
struct/enum/union 이 typedef alias 이름으로 symbol unit 방출.

- crates/kebab-parse-code/src/c.rs: type_definition 분기 추가.
  inner anonymous struct_specifier / enum_specifier / union_specifier
  탐지 → declarator field 의 type_identifier 재귀 추출 → synthetic
  unit (typedef alias). named inner aggregate / plain alias 는
  기존대로 glue. PARSER_VERSION code-c-v1 → code-c-v2.
  recover_typedef_alias + extract_typedef_alias_name helper 추가.

- crates/kebab-store-sqlite/src/store.rs: 두 helper 신규
  (parser_version bump cascade 용 doc-id 기반 orphan purge).
  - stale_chunk_ids_for_workspace_path_except_doc_id(workspace_path,
    keep_doc_id) — sister of stale_chunk_ids_at, doc_id 기반.
  - purge_document_at_workspace_path_except_doc_id(workspace_path,
    keep_doc_id) — CASCADE document/chunks 제거, assets 보존.
  keep_doc_id="" 가 "모든 doc 제거" 사용.

- crates/kebab-app/src/lib.rs: try_skip_unchanged 의 parser_mismatch
  분기에서 purge_workspace_path_for_parser_bump 호출. helper 가
  app.vector() 로 lazy 접근 + delete_by_chunk_ids + SQLite document
  row 제거. Ok(None) 반환 전 cleanup 끝나서 caller 의 새 INSERT 시
  idx_docs_workspace_path UNIQUE 충돌 회피.

- tests:
  - c.rs unit tests 4 신규 — typedef_struct_emits_unit /
    typedef_enum_emits_unit / typedef_union_emits_unit /
    typedef_to_existing_type_stays_glue (negative).
  - tier1_c_ingest_searchable: parser_version assertion code-c-v1 →
    code-c-v2.
- 회귀: bytes-edit 경로 (asset_id 변경) 의 기존 purge_orphan_at_workspace_path
  + purge_vector_orphans_for_workspace_path 는 그대로 — 신규 분기와
  공존, 기존 test 모두 PASS.

미해결 (Risks): nested typedef (typedef struct { struct {...} inner; }
Outer;) 의 inner 익명 struct 는 여전히 glue — v2 의 1차 범위는
top-level typedef alias 만.

cargo test --workspace --no-fail-fast -j 1 + clippy 통과.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:30:57 +00:00
fe123c0c6d test(A4): korean + english trigram matching at FTS level
3개 신규 unit tests in tests/fts.rs §7:

1. fts_trigram_korean_3char_substring_hits — Codex sqlite 3.45.1 검증
   동작 5개 assert pin: raw 3자 substring hit (충돌은/발생한),
   quoted phrase hit (\"해시 충돌\"/\"시 충\"), raw 해시충 0-hit (원문
   미존재).
2. fts_trigram_korean_short_query_zero_hit_pinned — 2자 한국어 query
   (충돌·키) 0-hit 회귀 감지. trigram 구조 변경 시 먼저 fail.
3. fts_trigram_english_substring_hits — substring recall 동작 변경
   pin (token→tokenizer, to 0-hit).

검증: cargo test -p kebab-store-sqlite --test fts → 13/13 PASS
(신규 3 + 기존 10).

Step 1c (multi-token 한국어 query e.g. \"해시 충돌\") 와 Step 5
(lexical BM25 snapshot 갱신) 는 Task A5 의 build_match_string()
재설계 후 진행.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:57:37 +00:00
8dcedc4b11 feat(p10-r2): V007 trigram migration + design §5.5 + fts diff-check
Task A2 + A3 한 묶음.

migrations/V007__fts_trigram.sql 신규:
- chunks_fts shadow 를 DROP + 재생성 (tokenize = trigram).
- chunks_ai/ad/au trigger 재생성 (V002 와 동일).
- chunks 에서 backfill INSERT — 사용자 re-ingest 불필요, V007 자동.
- V002 는 historical cold-upgrade replay 위해 그대로 유지.

design §5.5 갱신:
- verbatim block 의 tokenize 만 trigram 으로 교체.
- §5.5 본문 상단에 한국어 채택 사유 + trade-off (영어 lexical 변경,
  BM25 분포, 디스크 ~2-10x, contentless 아님) prose 한 단락 추가.

crates/kebab-store-sqlite/tests/fts.rs:
- fts_v002_matches_design_section_5_5_verbatim →
  fts_v007_matches_design_section_5_5_verbatim 으로 rename.
- extract_migration_5_5_verbatim_block() 의 include_str! path 를
  V007__fts_trigram.sql 로 변경. 주석/assertion msg V007 로.
- V002 cold-upgrade test 들 (fts_v002_backfill_*) 은 그대로 유지.

검증: cargo test -p kebab-store-sqlite --test fts → 10/10 PASS
(`fts_v007_matches_design_section_5_5_verbatim` 포함).

Codex round 1/2 의 design §5.5 contentless 정정·trigram tokenizer
채택 사유 명시 발견 반영.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 00:52:40 +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
4e8b84c4e0 fix(dogfood): populate schema.v1.repo_breakdown (Task 9 follow-up)
Dogfooding (PR #142 1B + multi-root corpus: kebab-docs + httpx + zod + lodash)
revealed schema.v1.repo_breakdown is always {} despite the 1A-2 Task 9
having added the code_lang_breakdown sibling. The schema.rs:171 placeholder
`BTreeMap::new()` was left in place. Mirror Task 9's code_lang_breakdown
query for the repo field — same metadata_json JSON-path pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:09:19 +00:00
918ee6c0be fix(p10-1a-1): apply code_lang + repo filters in lexical SQL and filter_chunks (dogfood-discovered)
p10-1A-1 (PR #139) added SearchFilters.code_lang + .repo fields and the CLI
--code-lang / --repo flags propagate them correctly into SearchFilters, but
neither the lexical retriever's FTS SQL nor the shared filter_chunks helper
(used by the vector retriever) ever applied them — so a code-lang-filtered
search returned all-doc hits (markdown / pdf / code mixed).

Discovered while dogfooding p10-1B with httpx + zod + lodash clones:
`kebab search 'AsyncClient' --code-lang python --json` returned markdown
hits from httpx/docs/ first.

Fix: add IN-list filters on json_extract(d.metadata_json, '$.code_lang')
and '$.repo' to both lexical.rs and filters.rs, mirroring the existing
media filter pattern. Two regression tests added in each crate covering
the new filter behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 03:27:01 +00:00
da51e59081 feat(p10-1a-2): populate schema.v1 code_lang_breakdown
Add `SqliteStore::code_lang_breakdown()` that queries
`json_extract(metadata_json, '$.code_lang')`, groups by it, and
skips NULL rows — returning `BTreeMap<String, u32>`.

Wire it into `collect_stats` in `kebab-app::schema`, replacing the
`BTreeMap::new()` placeholder inserted by 1A-1.

Test: `store::tests::code_lang_breakdown_counts_by_code_lang` asserts
rust=1 and that a null-code_lang doc does NOT appear in the map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:41:52 +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
126559ce7a fix(fb-40): update test fixtures for rag-v2 default 2026-05-10 19:15:15 +09:00