Commit Graph

670 Commits

Author SHA1 Message Date
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
ee39cfb387 chore: PR #234 회차 2 리뷰 반영 — doctor hint 노출 + 압축 가드
2회차 리뷰가 1회차 지적 3건을 모두 해결됐다고 확인하고 승인했다. 남은
MEDIUM 1건과 LOW 2건 중 값이 있는 것을 반영한다.

1) doctor hint 가 사람 눈에 안 보였다 (MEDIUM)

   렌더러가 `if let (false, Some(hint))` 로 실패한 체크의 hint 만 찍는다.
   회차 1 에서 `vector_store` 를 정보성(`ok: true`)으로 낮추면서, 정작 그
   경고를 전달할 유일한 경로를 막아 버렸다 — 문자열은 만들어지지만 `--json`
   에만 실리고 `kebab doctor` 를 그냥 돌린 사용자는 영영 못 본다.

   hint 가 있으면 항상 찍는다. 정보성 경고는 `✓` 도 `✗` 도 아닌 `!` 로
   표시해 실패와 구분한다.

2) `version_before` 읽기 실패 시 압축이 매번 걸렸다 (LOW)

   `table.version()` 실패는 `unwrap_or(0)` 이라, 버전이 압축 간격을 넘은
   스토어에서는 `after / n > 0` 이 항상 참이 되어 그 호출마다 압축이 돈다.
   `version_after` 쪽에만 있던 0 가드를 `version_before` 에도 넣었다.
   멱등이라 정합성 문제는 없지만 압축이 무거운 연산이라 비대칭을 남길
   이유가 없다.

3) 테스트 주석이 실제보다 한 단계 과장돼 있었다 (LOW)

   "modulo 트리거가 놓치는 경우" 라고 적었는데, modulo 로 되돌렸을 때
   실패하는지는 압축이 만드는 버전 수까지 얽힌 산술에 달려 있어 보장되지
   않는다. 이 테스트가 확실히 잡는 것은 "삭제 경로에 압축이 아예 없음"
   이므로 그렇게 고쳐 적었다.

미반영(후속): `flush_vector_deletes` 가 `crate::ingest::` 에 있어 reset 이
거기서 부르는 구조(호출부가 셋이 되면 옮긴다), `purge_deleted_workspace_path`
가 트랜잭션을 안 써서 남는 잔여 고아 창(이 PR 이 만든 문제도, 이 계층에서
닫을 수 있는 문제도 아니다).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 17:44:18 +09:00
1ff547f4c5 chore: PR #234 회차 1 리뷰 반영 — 압축 트리거·고아 창·doctor 판정
리뷰어 두 명이 독립적으로 같은 결함을 지목했고 실측으로 확인됐다.

1) 삭제 경로 압축이 실제로는 안 걸렸다

   `maybe_compact` 가 `version % compact_every == 0` 으로 판정했는데, writer
   마다 버전을 올리는 폭이 다르다. `upsert` 는 호출당 1 이라 배수를 반드시
   밟지만 `delete_by_chunk_ids` 는 200-id 배치마다 커밋해서 한 번의 호출이
   여러 칸을 건너뛴다. 이 PR 이 실측한 28474 → 28507 이 정확히 그 경우다
   (28507 % 512 = 347, 구간에 512 배수 없음). 즉 추가한 압축이 호출당
   1/512 확률로만 걸렸다.

   구간 교차 판정(`after / n > before / n`)으로 바꿨다. 쓰기 전 버전을
   인자로 받는다.

   기존 테스트도 이걸 못 잡았다 — id 를 하나씩 40회 나눠 불러 버전이 1씩
   올라가는, 이 PR 이 없앤 옛 형태였다. 한 번의 호출로 2,000 id(=10 커밋)를
   보내는 실제 출하 형태로 다시 썼고, modulo 판정으로 되돌리면 매니페스트
   12개로 실패한다.

2) 고아 벡터 창이 문서 1건에서 sweep 전체로 커졌다

   `execute_orphans_only` 는 purge 실패 시 `?` 로 즉시 반환하는데, 그러면
   루프 뒤의 배치 삭제가 아예 실행되지 않아 그때까지 버퍼에 쌓인 벡터가
   전부 고아가 된다. documents 행은 이미 지워져 다음 sweep 도 못 찾으므로
   회수 수단이 `reset --vector-only` 전량 재임베딩뿐이다. 기존 건별 삭제보다
   명확히 나빠지는 회귀였다.

   5,000 id 마다 flush 하고, reset 의 에러 경로도 flush 를 거쳐 반환한다.
   커밋 수는 여전히 문서 수의 수십 분의 1이라 #230 목적은 그대로다.

   더불어 HOTFIXES 의 "SQLite purge 는 crash-safety 경계" 는 사실이 아니라
   정정했다 — `purge_deleted_workspace_path` 는 트랜잭션을 열지 않고 DELETE
   세 개가 각각 autocommit 이다.

3) doctor 판정이 규모에 따라 뒤집혔다

   `meta_bytes > data_bytes` 는 절대 바이트 비교라, manifest 는 fragment 수의
   제곱으로 data 는 코퍼스 크기에 비례해 자라는 탓에 작은 노트 KB 가 오탐으로
   fail 했다. 리뷰어 계산으로 문서당 1청크면 105 문서부터 걸린다.

   보존 버전 수가 압축 간격의 2배를 넘는지로 바꿨다(규모 무관). 그리고
   판정을 정보성으로 낮췄다 — `DoctorReport.ok` 가 종료 코드 3 을 만들고
   스크립트·에이전트가 그걸로 분기하는데, 압축이 밀린 스토어도 질의에는
   정상 응답한다. 테이블을 못 찾았을 때도 체크를 남긴다(조용히 사라지면
   정상으로 읽힌다).

검증: 워크스페이스 186 결과 전부 통과. 실 KB doctor 출력 확인
(`200 fragments / 2832.3 MB data, 201 versions / 2.0 MB metadata`, 종료 코드
영향 없음).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 17:26:33 +09:00
ed6cfd606f fix(store-vector): #230 나머지 — 삭제 배치화 + 삭제 경로 압축 + doctor 지표
앞 PR(#233)이 #230 의 제안 2(압축 정책)만 닫았다. 남은 1·3·4 를 처리한다.

1) 삭제가 파일 1건당 Lance 커밋 1회 (제안 1)

   `sweep_deleted_files` 와 `execute_orphans_only` 가 루프 안에서 파일마다
   `delete_by_chunk_ids` 를 불렀다. 그 호출 하나가 Lance 커밋 하나라 문서
   삭제 수와 커밋 수가 1:1 이었다 — #230 이 보고한 "삭제 5,834건 → 커밋
   5,834회" 가 이것이다.

   chunk_id 를 sweep 전체에 걸쳐 모아 루프가 끝난 뒤 한 번만 부른다. SQLite
   purge 는 건별 커밋을 유지한다. 그쪽이 crash-safety 경계이고, 기존 정책이
   이미 중간에 죽으면 orphan 벡터가 남는 것을 허용한다.

   실 KB 실측(28,427 문서, 나무위키 샤드 하나 = 364 문서 삭제):
   Lance 커밋 364 → 33, 버전 번호 28474 → 28507, _transactions 336 → 369.
   33 은 `delete_by_chunk_ids` 의 200개 배치 상한에서 나온다(364 문서 ×
   약 18 청크 ≈ 6,500 id / 200). 커밋 수가 문서 수가 아니라 청크 수에
   비례하게 됐다.

2) 삭제 경로에도 압축

   압축이 upsert 에만 걸려 있어 `reset --orphans-only` 처럼 삭제만 하는
   경로는 여전히 무한 누적이었다. 트리거를 `maybe_compact` 헬퍼로 빼고
   `delete_by_chunk_ids` 끝에서도 부른다.

3) doctor 에 Lance 지표 (제안 4)

   `vector_store` 체크 추가 — fragment 수 / 데이터 크기 / 버전 수 /
   메타데이터 크기를 보여주고 메타데이터가 데이터보다 크면 fail 로 잡는다.
   그게 #230 병리의 모양이다. lancedb 를 열지 않고 파일시스템만 읽어서
   임베딩 provider 가 none 이어도 나오고 비용이 0 이다. `DoctorCheck` 목록에
   항목을 더하는 것이라 `doctor.v1` wire 는 그대로다.

4) geodatafusion (제안 3) 은 미해결

   lance 내부라 kebab 쪽 feature flag 가 없다. 다만 이 비용은
   `table.delete()` 호출당 fragment 수만큼 곱해지므로 위의 커밋 수 감소가
   그대로 곱셈 횟수 감소다. 근본 해결은 upstream 이슈.

부수 관찰: 364 문서 삭제에 38.7분이 걸렸는데 Lance 쪽은 33 커밋뿐이므로
남은 비용은 #229(chunks_fts 삭제가 FTS5 전체 스캔, 문서당 약 6초)다.
364 × 6초 ≈ 36분으로 거의 전부 설명된다. #230 을 고쳐도 sweep 체감이
안 바뀌는 이유가 이것이다.

검증: 워크스페이스 186 결과 전부 통과. 신규 회귀 테스트
`repeated_deletes_do_not_accumulate_lance_versions` 는 삭제 경로 압축을
빼면 40회 삭제에 매니페스트 42개로 실패한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 17:02:01 +09:00
2ad99ab491 chore: PR #233 리뷰 반영 — 압축 트리거·플래그 이름·불필요 의존 정리
리뷰에서 나온 지적 중 실제로 고칠 값이 있는 4건 반영.

1) 압축 트리거를 인메모리 카운터 -> Lance 테이블 version 으로

   `upserts_since_compact: AtomicU64` 는 store 인스턴스 수명 동안만 살아
   있는데, `kebab ingest-file` 과 MCP `ingest_file`/`ingest_stdin` 은 호출마다
   새 App(따라서 새 store)을 연다. 한 번에 수만 건 넣는 ingest 에서만 512 에
   닿고, 한 건씩 넣는 경로에서는 카운터가 매번 0 으로 되돌아가 압축이 영영
   안 돌았다 — PR 이 잡겠다던 fragment 누적이 그 경로에 그대로 남는 셈.

   Lance 의 `table.version()` 은 테이블에 저장돼 프로세스를 넘어 단조 증가
   하므로 그걸 기준으로 바꿨다. 부수적으로 struct 에서 카운터 필드와
   AtomicU64 import 가 사라졌다.

2) `--fail-under` -> `--max-drop`

   관용적으로 `--fail-under 0.9` 는 "지표가 0.9 미만이면 실패" 로 읽히는데
   실제 의미는 "0.9 이상 떨어지면 실패" 였다. 그대로 두면 CI 에 하한이라고
   믿고 적은 값이 아무것도 안 막는다 — recall 0.95 -> 0.10 붕괴도 낙폭
   0.85 < 0.9 라 통과. 새로 노출되는 표면이라 지금이 바꿀 수 있는 시점이다.
   음수·nan 은 시작 시점에 거부한다(그대로 두면 게이트가 무력화됨).

3) `chrono` 직접 의존 제거

   lancedb 가 `chrono::Duration` 을 이미 re-export 한다
   (lancedb-0.23.1/src/table.rs:85). 앞 커밋이 Cargo.toml 에 적은
   "lancedb 는 chrono 를 re-export 하지 않는다" 는 사실이 아니었다.

4) 안전성 근거 주석 정정 + 측정치 통일

   `delete_unverified: true` 의 근거를 "kebab ingest 는 단일 동기 프로세스"
   라고 단언했으나, 이 리포는 `kebab mcp` 를 preferred 통합 표면으로 배포하고
   그 서버는 ingest 까지 노출한다. 락도 없다. 단언 대신 전제와 그 전제가
   코드로 강제되지 않는다는 사실, 그리고 이 플래그 없이는 신규 색인에서
   아무것도 회수되지 않는다는 trade-off 를 그대로 적었다.
   압축 소요는 사본 측정치(102초) 대신 실 테이블 측정치(153초)로 통일.

`is_multiple_of` 는 MSRV(1.85) 보다 높은 1.87 안정화라 `%` 로 대체.

검증: 워크스페이스 186 결과 전부 통과 · 실패 0. `--max-drop` 3경로 실증
(통과 exit 0 / 잘못된 값 exit 2 / 회귀 exit 1). 회귀 경로에서 "측정 불가"
가드가 실제 run 쌍으로 발동하는 것도 확인.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 14:56:26 +09:00
ce10530003 fix: 28.4k 문서 도그푸딩이 잡은 결함 3건 + eval 회귀 게이트
나무위키 18,282 + Apache Jira 10,145 = 28,427 문서 / 600,808 청크로 종단
도그푸딩. 기존 최대 규모(620 문서)의 46배라 그 규모에서 안 보이던 결함이
드러났다. 상세 evidence 는 tasks/HOTFIXES.md 2026-08-16 엔트리.

1) Lance fragment 무한 누적 (이슈 #230 제안 2)

   upsert 가 asset 1건당 merge_insert 를 1회 호출하고 Lance 는 그때마다 새
   버전 + fragment 를 만든다. manifest 는 그 시점의 전 fragment 를 나열하므로
   N번째 쓰기가 N줄짜리 manifest 를 새로 쓴다 — 쓰기 비용이 문서 수에 비례,
   manifest 총량은 제곱. 코드베이스에 optimize/compact/cleanup 호출이 0건이었다.

   16,828 문서 시점: fragment 16,814 · _versions 12.2 GB (실데이터는 1.7 GB) ·
   ingest 30.7 → 4.3 문서/분으로 단조 하락.
   COMPACT_EVERY_N_UPSERTS=512 마다 Compact + Prune 추가 후: manifest 336 ·
   _versions 6.6 MB · 28,427 문서 끝까지 32~36 문서/분 유지.

   기존 테이블 1회 압축은 153초에 15 GB → 1.7 GB (행 365,991 보존).
   같은 이슈의 삭제 경로 배치화 / geodatafusion / doctor 지표는 미해결.

2) 묶음 인용 마커가 answer.v1 에서 조용히 사라짐

   마커 정규식이 괄호 하나에 마커 하나만 인정해서, 모델이 한 주장에 여러
   근거를 다는 [#2, #10] 형태를 못 잡았다. citations 는 추출 마커와 packed
   entry 의 교집합이라 그 근거들이 배열에서 빠지고, 본문에는 [#2] 가 남아
   해소 불가능한 인용이 됐다. 프롬프트는 [#번호] 로 귀속하라고만 하고 한
   괄호에 하나씩 쓰라고 지시한 적이 없으므로 모델 잘못이 아니다.

   실측: 본문 1,2,6,7,8,9,10 vs citations 1,8,9,10 → 3건 끊김. 수정 후 7/7.
   기존 엄격함(vec![1] · [1] · [ #1 ] · [#foo] · [#1234] 불인정)은 유지.

3) ollama 요청이 max_tokens 를 안 보냄

   GenerateRequest::max_tokens 를 RAG 파이프라인이 계산해 넘기는데
   OllamaOptions 가 그걸 직렬화하지 않았다. ollama 기본 num_predict 는
   -1(무제한)이고 컨텍스트가 차면 창을 밀어 계속 생성한다. 연결에 바이트가
   계속 흐르므로 request_timeout_secs 로도 못 막는다.

   eval run --with-rag 216 질의가 1시간 45분에 21개만 끝냈고, 붙잡고 있던
   질의 하나가 13 MB 를 받은 상태였다. num_predict 전송 후 같은 216 질의가
   31분에 완료.

4) eval compare --fail-under (신규)

   이전에는 delta 만 출력하고 exit code 가 항상 0 이라 "회귀했는가"를 기계가
   판정할 수 없었다. empty_result_rate 는 반대 방향으로 검사하고, A 에서 재던
   지표가 B 에서 NaN 이 되면 위반으로 잡는다 — 골든셋이 ground truth 를 잃은
   경우가 정확히 그 모양이라 조용히 통과시키면 안 된다.

테스트: 워크스페이스 186 결과 전부 통과. 신규 회귀 테스트 7개
(compaction 1 · 마커 그룹 1 · num_predict 1 · fail-under 4).
clippy 는 kebab-parse-code 의 기존 question_mark 지적으로 red 인데 main 도
동일하며 이 PR 이 건드리지 않은 크레이트다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 14:08:27 +09:00
93e69c9bec docs: 재조정 후속 — 정책 문구 정합 + 코드 코멘트 lag 수정
spec 재조정(이전 commit)으로 생긴 정책↔실제 모순 + 검증이 발견한 코드 lag 정리.

- CLAUDE.md / DOCS.md: "frozen 편집 금지" → "reconciled baseline" 으로 정합.
  원칙 동결이되, 누적 drift/삭제로 문서가 현실과 모순되면 주기적 dated 재조정
  (✂/⟳/⚠)을 허용·기대. HOTFIXES 는 재조정 사이 fine-grained live deviation log.
- kebab-rag/src/pipeline.rs:580: doc-comment rag-multi-hop-v1 → v2
  (실제 const PROMPT_TEMPLATE_VERSION_MULTI_HOP 와 일치 — 검증 패스 발견).

코멘트 전용 변경이라 빌드 영향 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5r6DDehC3ECcC2BxLsPHu
2026-06-27 23:09:28 +00: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
cf773ad56c chore: bump version 0.31.0 → 0.32.0 + v0.32.0 release notes
ponytail-audit over-engineering 정리 arc(#219–#222) 릴리스. 능력 불변,
누적 −3200줄 / crate 24→20 / dep −2(serde_yaml + unsafe-libyaml).

- workspace version 0.31.0 → 0.32.0 (minor — #219 의 CLI 플래그/config 키
  제거가 인터페이스 변경 트리거). 전 kebab-* crate 자동 cascade.
- docs/release-notes/v0.32.0-draft.md: 4 surface(죽은 cache scaffold 제거 /
  chunker 통합 / shim crate 흡수 / FusionPolicy·YAML·NLI) 별 설명 + 도그푸딩.
- tasks/HOTFIXES.md 2026-06-27: chunker A/B byte-identical(pre/post-arc 실코퍼스
  120==120 diff-empty) + GPU r9700 arctic ingest + CLI 표면 evidence.
- kebab eval stale 주석 정정(PR5-A: "placeholder; lands in P9" → 실제 배선된
  검색/RAG 품질 측정 suite). 게이팅 안 함(live subcommand 유지).

도그푸딩 evidence 가 bump commit 이전에 HOTFIXES + release notes 에 명시됨
(CLAUDE.md §Dogfood trigger).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 03:15:34 +00:00
bac541ba90 chore: PR #222 회차 1 리뷰 반영 — serde_yaml_ng workspace pin 복원
리뷰 nit: dropped serde_yaml 은 workspace-pinned 였는데 대체 serde_yaml_ng 가
3 crate 에 개별 "0.10" 하드코딩이라 single-version-pin invariant 깨짐.
`[workspace.dependencies]` 에 serde_yaml_ng = "0.10" 추가 + kebab-chunk/eval/
parse-md(사전존재 하드코딩 포함) 를 { workspace = true } 로 통일 — 워크스페이스의
다른 공유 dep 과 동일 컨벤션. 빌드 dep resolution 불변(0.10.0 단일).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 02:50:43 +00:00
3ac3e91421 refactor: audit-tail 정리 — FusionPolicy inline + dual-YAML 통일 + NLI required
ponytail-audit 의 작은 over-engineering 잔여 3건 (output/동작 불변):

1. FusionPolicy 1-arm enum 제거 — kebab-search 내부 전용(외부 importer 0)이던
   `enum FusionPolicy { Rrf { k_rrf } }` 를 `HybridRetriever.k_rrf: u32` 로 inline.
   parse_fusion 은 u32 반환. RRF 수식 `Σ 1/(k_rrf+rank)` byte-identical.
2. dual YAML 통일 — serde_yaml(0.9) 를 maintained fork serde_yaml_ng(0.10) 로 일원화.
   k8s manifest chunker + eval golden loader 2 site 마이그레이션(API drop-in),
   workspace dep 에서 serde_yaml 제거. parse 결과 불변.
3. NliVerifier::hypothesis_token_count 의 `Ok(0)` default 제거 → required.
   prod OnnxNliVerifier 가 이미 override 라 동작 불변, MockNliVerifier 에 explicit 추가.

문서 동기화: search/eval/parse component README + ARCHITECTURE 의 FusionPolicy/
serde_yaml 참조를 현 상태로 갱신. net −37줄. 적대적 검증 2렌즈(output-identity +
completeness-build) 통과, 영향 crate 테스트 + 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-27 02:44:03 +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
cc17873d08 chore(chunk): PR #220 회차 1 리뷰 반영 — .context → .with_context (lazy alloc)
리뷰 nit: collapsed AST chunk arm 의 .context(format!(...)) 는 성공 경로에서도
String 을 매번 할당. .with_context(|| format!(...)) 로 에러 경로에만 alloc.
(나머지 nit — 일반화된 error-text "CodeAstV1Chunker" — 은 리뷰어가 no-action:
테스트가 새 substring 으로 맞춰져 있고 7개 arm 과 .context prefix 통일됨.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 00:05:20 +00:00
7665272aa5 refactor(chunk): 9개 동일 code AST chunker → CodeAstV1Chunker 통합
code_{c,cpp,go,java,js,kotlin,python,rust,ts}_ast_v1.rs 9개는 struct 이름과
const VERSION_LABEL 문자열만 다른 byte-identical 복붙이었다 (chunker 는
tree-sitter 를 안 쓰고 lang 은 SourceSpan::Code 데이터에서 흘러나옴). 하나로
통합:

- 신규 CodeAstV1Chunker { version_label: &'static str } + for_lang(lang) 생성자.
  9개 lang → 기존 VERSION_LABEL 문자열을 verbatim 매핑 (rust→code-rust-ast-v1,
  typescript→code-ts-ast-v1, javascript→code-js-ast-v1, ...). chunker_version()
  이 라벨을 그대로 반환 → chunk_id (id_for_chunk) byte-identical → 재인덱싱·
  버전 캐스케이드 0.
- chunk/policy_hash/make_chunk/split_oversize 는 code_rust_ast_v1.rs 에서 verbatim
  이식. 인라인 유닛 테스트 1세트 + 9-lang 라벨 매핑 테스트 추가.
- 9개 src 파일 삭제, lib.rs 9 mod+pub use → 1.
- ingest.rs 의 두 디스패치(chunker_version 스탬프 ~2700 + .chunk() ~3298)에서
  9개 AST arm 을 단일 arm 으로 collapse, for_lang(code_lang) 사용. 비-AST
  chunker(K8s/Dockerfile/Manifest/TextParagraph) arm 은 무손상.

byte-identical 증명: 9개 골든 스냅샷 테스트(tests/code_*_ast_snapshot.rs)가
assertion/expected-data 수정 0으로 통과 — construction/import 줄만 변경.
net −3030줄 (20파일 +143/−3173). 적대적 검증 3렌즈(byte-identical-proof +
version-string-integrity[9 라벨 old==new] + completeness) 통과, 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 23:59:10 +00: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
db9090c87b refactor(search): 죽은 search-cache + search --explain scaffold 제거
in-process LRU search cache 는 spine 재작성(#214)에서 이미 삭제됐는데
그 비계(scaffold)만 남아 있었다. 잔존물 제거:

- App::search/search_uncached 붕괴 (search() 는 1줄 위임자였음)
- search_uncached_with_config facade + 그 테스트 삭제
- 죽은 `search --no-cache` / `search --explain` CLI 플래그 제거
  (ask --explain 은 live — 건드리지 않음)
- 안 읽히던 RagCfg.explain_default config 필드 + fixture 제거
- 제거된 표면을 가리키던 stale 주석/문서(citation_helper/hybrid/
  search·app-facade README/DOGFOOD/HANDOFF) 정정

코어 검색 출력은 byte-identical (search_uncached 본문이 search() 로
verbatim 이동). `search_cache: false` wire capability 는 유지 — 제거 시
schema.v1 breaking bump 이라 손해. 적대적 검증 2렌즈(correctness-preserved
+ wire/config-safe) 통과, dead-symbol-complete 가 잡은 5건 주석/문서 정정 완료.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-26 17:12:55 +00:00
602d752d37 test(app): caption 캐시 b2 헬퍼 추출 + mock-LLM 결정적 게이트 (재실행=LLM 미호출) 2026-06-25 04:43:38 +00:00
141dfbfd3e refactor(app): PDF per-page OCR 캐시 GET 에러 debug 로그 — b1/b2 와 관측성 대칭 (PR #217 회차 1) 2026-06-25 01:34:27 +00:00
68138049ad test(app): paddle 실엔진 이미지 OCR 캐시 #[ignore] 게이트 — 재인덱싱=히트 (b1 e2e) 2026-06-25 01:23:17 +00:00
f375b694b6 test(app): OCR 캐시 mock-engine 결정적 검증 — 재실행=엔진 미호출 + byte-identical + 버전 무효화 2026-06-25 01:13:02 +00:00
e64a56a321 feat(app): PDF 페이지 OCR 캐시 — PdfOcrOpts 캐시 핸들 (b3, parse-crate 의존 무변경) 2026-06-25 01:06:47 +00:00
578e78c83f feat(app): 이미지 caption 결과 캐시 — 바이트 키 caption 네임스페이스 (b2) 2026-06-25 00:56:45 +00:00
6ab503e27f feat(app): 이미지 OCR 결과 캐시 — 바이트 키 ocr 네임스페이스 (b1) 2026-06-25 00:54:20 +00:00
5ed80acf38 feat(app): ocr/caption derivation payload — full-struct serde encode/decode 2026-06-25 00:44:19 +00:00
7ed0151b81 feat(core): derivation_cache_key_bytes — 바이트 키 변형 (OCR/caption 입력용, NFC 없음) 2026-06-25 00:38:03 +00:00
ec2ee25a89 test(app): 캐시-히트 테스트에 단일-자산 격리 단언 추가 (report.new==1) — PR #216 회차 1 2026-06-24 23:53:38 +00:00
7310a8a30a test(app): code 재인덱싱 임베딩 캐시 히트 + byte-identical 검증 (AVX-gated) 2026-06-24 23:38:03 +00:00
ff4f9b782d feat(app): image/pdf/code 임베딩 캐시 hit/miss 로그 — markdown 과 관측성 대칭 2026-06-24 23:25:13 +00:00
2517776b4a feat(app): image/pdf/code 임베딩을 embed_with_cache 경유로 (markdown 패턴 통일) 2026-06-24 23:19:19 +00:00
cebfc893ad refactor: 제거 기능 stale surface 정리 — 세션/wire 필드/rag_multi_turn/crate수 (PR #214 회차 1)
cut 으로 제거된 기능이 diff 밖 contract surface 에 남긴 잔재 정리:
- integrations/claude-code/kebab/SKILL.md: 제거된 --session/session_id 안내,
  answer.v1 의 conversation_id/turn_index 필드 목록, rag-v2 기본 pin, kebab tui
  언급 제거. 기본 템플릿 rag-v4(대체 rag-v3)로 갱신.
- docs/mcp-usage.md: ask 입력 스키마의 session_id, 'Session 관리(multi-turn)'
  섹션 전체(chat_sessions/chat_turns 포함), 예시의 conversation_id/turn_index 제거.
- docs/wire-schema/v1/answer.schema.json + answer_event.schema.json: Answer 가
  더는 방출 않는 conversation_id/turn_index 속성 제거(producer/doc 괴리 해소).
- schema.rs + wire.rs: capabilities.rag_multi_turn true→false(search_cache 와
  동일 처리; frozen v1 맵이라 키는 유지, 값만 flip).
- CLAUDE.md: 24→22 crates(tui+candle crate 제거 반영).
2026-06-24 15:53:50 +00:00
0ae739eeaf fix(config): v4→v5 마이그레이션이 image-only Option OCR 키를 pdf 로 누출 (PR #214 회차 1)
endpoint/det_model/rec_model/dict 는 default None 이라 reconcile 가 pdf 블록에
채우지 않는다(None 은 annotated-default 에서 누락). image 만 설정한 이 키를 공유
[ingest.ocr] 로 끌어올리면 resolve_ocr overlay 가 (medium_has(pdf,key)=false 라)
pdf 로 누출돼, pdf OCR endpoint/asset 경로가 v4 와 달라지고 det/rec/dict 는
ingest_config_signature 에 들어가 강제 재색인까지 유발했다. step_4_to_5 에서
Option 키는 pdf 도 명시한 경우에만 hoist 하도록 OPTION_OCR_KEYS 가드 추가.

기존 round-trip 테스트는 before=Config::from_file(v4) 가 이미 마이그레이션+resolve
를 거친 오염값이라 after==before tautology 였음 — pdf.det_model==None 명시 검증
추가 + 비대칭 image-only-endpoint 회귀 테스트 신설.
2026-06-24 15:46:20 +00:00
00ead29720 refactor(app): ingest 코드를 src/ingest 모듈로 분리 — lib.rs 4331→493줄, API 무변, 재인덱싱 게이트 IDENTICAL 2026-06-24 14:23:32 +00:00
2a0a30aa1c refactor(app): chunk_asset stage 헬퍼 — 청커 선택+tier3 fallback 통합 2026-06-24 13:57:17 +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
9f40c88722 refactor(app): fingerprint_and_skip — effective version+skip 결정 중앙화 (4× 복제 제거) 2026-06-24 13:06:42 +00:00
0f9a76997d refactor(app): ingest store 시퀀스를 store_asset 헬퍼로 추출 (4× 중복 제거) 2026-06-24 12:49:09 +00:00
d501ce2626 refactor(app): ingest API 6변종 → 2 (ingest + ingest_with_config{IngestOpts})
summary_only를 IngestOpts에 흡수, progress/cancellable/opts 변종 제거.
재인덱싱 게이트 CHUNKS/SEARCH/ASK byte-IDENTICAL, clippy --all-targets 0.
2026-06-24 12:36:20 +00:00
2dfbbe4f43 refactor(config): consumer들이 &Config 대신 타입 슬라이스 수령 (god-struct 결합 해소) 2026-06-24 11:55:10 +00:00
bf7769cf5a refactor(config): env 97→~25 + 노출 키 109→~30 (표면 정리)
apply_env 의 KEBAB_* 매치 암을 103개→22개로 정리. 삭제된 암은 런타임에서
바꿀 일이 거의 없는 per-field 튜닝 노브(score_thresh, rrf_k, temperature,
multi_hop_*, nli_threshold, chunker_version, context_tokens 등) — struct
field 와 serde default 는 그대로 유지하므로 TOML 로는 여전히 설정 가능.

유지한 22개: endpoint×3, 모델명/프로바이더×5, 경로×2, 병렬도×2,
청킹 target/overlap×2, OCR 엔진/모델/언어+per-medium enabled×5,
caption enabled×1, search default_k×1, rag prompt_template_version×1.

영향 범위: struct field 삭제 없음(defaults 불변) → search/ask/chunk 출력
byte-identical 확인 (parity gate u2-surface IDENTICAL ✓).

docs: README KEBAB_* 항목을 실제 노출 키 목록으로 교체.
      SMOKE.md config 예시를 ~30개 공통 키로 슬림화 + env 설명 갱신.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 11:21:11 +00:00
da323af87b refactor(config): OCR 중복 제거 — 공유 [ingest.ocr] 엔진 블록 + v4→v5 마이그레이션
Phase 2 Unit 1. OcrCfg(image) 13필드가 PdfOcrCfg(pdf) 와 전부 중복(image 고유
0, pdf 고유 4) + apply_env 에 KEBAB_IMAGE_OCR_*/KEBAB_PDF_OCR_* 27 arm 복제를
제거한다.

- 신규 SharedOcrEngineCfg(13 공유 필드, 전부 Option, default None) = [ingest.ocr].
  엔진 설정 단일 출처. image/pdf 블록은 on/off 토글 + override.
- load-time resolution(Config::resolve_ocr, from_file 호출): 공유 필드가 Some 이고
  미디어 블록이 그 키 미명시면 concrete OcrCfg/PdfOcrCfg 로 overlay(presence 는
  toml::Value 로 판정; 미디어 > 공유 > 내장 default). struct 필드는 그대로 두고
  엔진 필드에 #[serde(default)] 만 추가(slim 블록 파싱) → image(gemma4:e4b/1600)
  vs pdf(qwen2.5vl:3b/2048) 미디어별 기본값 보존.
- resolver Config::image_ocr()/pdf_ocr() 추가. consumer(kebab-parse-image,
  kebab-app build_*_ocr_engine·ingest gate·pdf_ocr_apply·ingest_config_signature)가
  전부 경유 → god-struct 직접 read 제거.
- apply_env: 27 arm → 공유 KEBAB_OCR_* 12 arm(image+pdf 동시) + pdf 고유 4 arm +
  미디어별 KEBAB_IMAGE_OCR_ENABLED/KEBAB_PDF_OCR_ENABLED.
- step_4_to_5: [ingest.image.ocr] 12 엔진 키를 [ingest.ocr] 로 move_table(enabled
  제외). pdf 블록 무손상(reconcile 이 채워 공유 overlay 오염 X). annotated_default
  도 동일 통합으로 v5 canonical 형상. CURRENT_SCHEMA_VERSION=5.
- v4→v5 round-trip 테스트(비-default image engine 보존 + pdf 오염 X + 멱등). effective
  OCR 바이트 동일 → ingest_config_signature 불변 → 강제 재색인 없음.

검증: clippy --workspace --all-targets 0 / kebab-config·kebab-parse-image·
kebab-parse-pdf·kebab-app 테스트 pass. surface: README [ingest.ocr] 절 + SMOKE
config 블록 + DOGFOOD env + HOTFIXES dated entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 11:03:39 +00:00
0af9d58c95 test: 삭제된 심볼 참조 정리 (session_id, rag-v1→v4, cache_capacity) — worker cargo check가 test 타깃 미컴파일로 누락 2026-06-24 10:11:13 +00:00
3c3dcc86cd refactor: kebab-tui crate + tui 서브커맨드 제거 (UI=CLI/MCP) 2026-06-24 09:59:54 +00:00
040d17f122 refactor(embed): candle provider/crate 제거 — fastembed+ollama로 충분
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 09:59:13 +00:00
1d32aa645a refactor: multi-turn 세션 제거 (ask --session, chat_sessions/turns) + V015 drop migration 2026-06-24 09:57:12 +00:00
470e94bb2d refactor(app): search LRU 캐시 제거 (p9-fb-19) — 동작 불변, 표면 축소
- App::search() → search_uncached() 직통 (캐시 로직 전체 삭제)
- SearchCacheKey struct + impl 제거
- App.search_cache 필드 + 초기화 블록 제거
- build_cache_key() / clear_search_cache() 제거
- config SearchCfg.cache_capacity + default_cache_capacity() 제거
- CLI clear_search_cache() 호출 제거; --no-cache 플래그는 유지(no-op)
- wire/schema search_cache 값 true → false (필드 유지)
- lru workspace dep + kebab-app direct dep 제거
- unicode-normalization dep 유지 (first_question_title() 에서 사용 중)
2026-06-24 09:48:54 +00:00
7c935c6b96 refactor(rag): legacy 템플릿 rag-v1/v2 제거 — v3/v4만 유지 2026-06-24 09:45:17 +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
ed8ab7cdbe feat(chunk): pdf-page-v1.2 — PDF 페이지 oversize 분할 + 공유 oversize 모듈
md-heading-v2(PR #209)의 oversize 분할을 PDF 청커에도 적용. v1.1 의 chunk_page 는
문장/문단 경계로만 잘라서 경계 없는 거대 페이지(빽빽한 scanned page 한 줄 OCR)가
통째로 한 청크 → strict 임베더 실패. v1.2 는 2-tier: tier-1(문장/문단 greedy +
overlap) 후 segment 가 max_chunk_tokens 초과면 tier-2 가 공유 text_pieces 로
재분할 → 모든 PDF 청크 ≤ 예산.

- 신규 공유 모듈 crate::oversize (text_pieces/char_pieces/BYTES_PER_TOKEN) — md 와
  PDF 가 공유(단일 진실 공급원). md-heading-v2 는 호출만, 출력 byte-identical(md
  라벨·동작 불변, parity 테스트 전부 통과).
- PdfPageV1Chunker { max_chunk_tokens } + policy_hash budget fold(md 동형) +
  pdf_chunker_from_config(kebab-app). 신규 config 키 없음.
- 분할 조각 chunk_id 는 #c{segment_start}s{i}(미분할 단일 segment 는 bare
  #c{segment_start} 유지 → 공통 경우 v1.1 동일).
- Page span: 분할 조각은 부모 segment 의 char 범위를 그대로 가짐(segment-granular,
  md 동형). per-piece narrowing 은 text_pieces 의 줄 구분자 소실로 drift 하는
  버그라 코드 리뷰 후 제거 — 회귀 테스트
  oversize_pdf_page_with_newlines_splits_without_span_drift 로 잠금.
- chunker_version v1.1→v1.2 → 다음 ingest 에서 PDF 1회 자동 재청크(md/code 무영향).

검증: kebab-chunk lib 93 pass(span 회귀 포함), kebab-app green, clippy 0. 도그푸딩
(실험 KB, scanned PDF + arctic@Lemonade, budget 200): 625/625 errors=0,
scanned_page1 1→3 청크·scanned_page2 3→7 청크(둘 다 pdf-page-v1.2), 전 코퍼스
10215 청크 전부 ≤200. 버전 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:47:09 +00:00