Commit Graph

127 Commits

Author SHA1 Message Date
2c2cbd2b94 chore: PR #236 회차 1 리뷰 반영 — sweep 뒤 진행바 복구
리뷰 두 건에서 나온 지적을 반영한다.

1) sweep 이 끝나도 asset 진행바가 복구되지 않았다 (HIGH)

   sweep 은 asset 진행바를 빌려 쓰면서 자기 라벨과 자기(더 작은) 총계를
   씌운다. 그런데 `AssetStarted` 는 위치와 메시지만 세팅하고 길이·스타일은
   건드리지 않는다. 그래서 sweep 이 한 번 돌면 그 뒤 색인 구간 전체가
   `sweep [====] 4213/21` 로 그려졌다 — 라벨도 분모도 틀린다.

   더 나쁜 건 스타일 교체가 v0.26.1 의 커스텀 키 `{asset_elapsed}` 를 같이
   날린다는 점이다. 느린 asset 에서 `(Ns)` 가 도는 게 "멈춘 게 아님"의 유일한
   신호인데 sweep 이 그걸 없앤다. 이 PR 이 sweep 구간에서 없앤 "hang 처럼
   보임" 을 asset 구간에 새로 만드는 셈이었다.

   바 세팅을 `dress_bar_for_assets` 로 빼고 `ScanCompleted` 와
   `SweepCompleted` 양쪽에서 부른다. 스타일을 길이보다 먼저 세팅하는데,
   indicatif 가 두 호출 사이에 다시 그릴 수 있어 과도기 프레임이 최소한
   올바른 라벨을 달게 하기 위해서다.

   TTY 전용이라 비-TTY 실측만 보고 있어서 놓쳤다. pty 로 재현해 고친 뒤
   `ingest [====] 16/17 doc9.md` 로 나오는 것을 확인했다.

2) docs/DOGFOOD.md §1.8 이 갱신되지 않았다 (MEDIUM)

   `--json` 이벤트 순서 목록에 sweep 3종이 빠져 있었다. verify 항목에
   sweep 분모·연속성과 "sweep 뒤 진행바가 asset 분모로 돌아오는가" 를 넣었다
   — 1번이 정확히 그 항목이 있었으면 잡혔을 결함이다.

3) ingest_progress.rs 의 ordering invariant 주석이 stale 했다 (MEDIUM)

   §2.4a 순서 블록에 sweep 이 없었다. 설계 문서 자체는 frozen baseline 이고
   HOTFIXES dated entry 가 있으니 규약상 문제없지만 코드 주석은 living 이다.

4) 잔가지 (LOW)

   - purge 실패가 "디스크에 남아 있어 그냥 뒀다" 와 구분되지 않았다. 둘 다
     `removed: false` 이고 ndjson 에는 아무것도 안 남아,
     `sweep_summary` 의 `checked - purged` 차이로도 못 가른다. 사후 기록이
     로그뿐이라는 게 이 PR 의 전제이므로 `purge_failed` 를 추가했다.
   - `SweepProgress` 가 purge 할 때만 바 메시지를 세팅하고 비우지 않아,
     마지막 purge 경로가 이후 후보를 훑는 내내 남았다. 안 지울 때는 비운다.
   - 주석과 스키마가 신규 이벤트를 `v0.33.0` 이라고 적었는데, CLAUDE.md 의
     bump 규칙상 이 변경은 patch 다 (additive-only wire + 관측성 개선, 새
     명령·플래그·config 없음, 검색·색인 결과 불변 — 선례가 asset_phase).
     `v0.32.1` 로 고쳤다.

미반영: `SweepCompleted` 가 TTY 에서 `bar.println` 대신 stderr 에 직접
쓰는 것 — 기존 `AssetTimings`/`PdfOcr*` 이 같은 패턴이라 신규 회귀가 아니고,
바꾸려면 그 셋을 같이 옮겨야 해서 별 건이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 21:18:15 +09:00
1626a40a28 feat(app): #228 삭제 sweep 을 진행바·ndjson 로그에 노출
`sweep_deleted_files` 는 walker 가 끝난 직후 asset 루프 전에 도는데, 이
구간이 관측 가능한 신호를 하나도 내지 않았다. 진행바는 walker 총계
(`0/12115`)를 표시한 채 멈춰 보이고 ndjson 로그는 0바이트로 남는다.
`tracing::info!` 은 나가지만 wire 이벤트가 아니라 사용자가 볼 산출물이
없다. 프로세스는 CPU 100% 로 정상 동작 중인데 밖에서는 hang 과 구별할 수
없고, 실제 도그푸딩에서 세 번 연속 Ctrl-C 로 죽였다.

`ingest_progress.v1` 에 세 이벤트를 추가한다 (additive — 기존 소비자는
모르는 kind 를 무시한다).

  sweep_started   { total }
  sweep_progress  { idx, total, path, removed }
  sweep_completed { checked, purged, ms }

`total` 은 `all_workspace_paths()` 에서 이번 스캔이 덮은 경로를 뺀 값이라
루프 진입 전에 확정 분모가 나온다 (이슈 제안 1). `removed` 는 "정말 없어서
문서를 지웠다" 와 "아직 디스크에 있어 그대로 뒀다" 를 가른다 — 수천 건을
훑고 하나도 안 지우는 sweep 이 있으므로, 지울 때만 움직이는 진행바는 바로
그 경우에 다시 멈춰 보인다.

`purged` 를 두 이벤트에서 다른 타입으로 쓰지 않으려고 sweep_progress 쪽은
`removed`(bool), sweep_completed 쪽은 `purged`(정수) 로 이름을 나눴다.
같은 wire 키가 두 타입을 갖는 건 소비자 입장에서 함정이다.

ndjson 로그에는 `purge { ts, doc_path }` 와 `sweep_summary { ts, checked,
purged, ms }` 를 추가한다 (이슈 제안 2 가 요청한 형태). 로그가 유일한 사후
기록이다 — tracing 은 stderr 로 흘러가고 남지 않는다.

CLI 는 sweep 을 asset 진행바와 별 phase 로 그린다. 후보 12k 를 훑는 일과
asset 12k 를 색인하는 일은 분모가 다른 별개의 작업이라 카운터를 공유하면
두 번째 구간이 처음부터 다시 시작하는 것처럼 보인다. 비-TTY 는 실제로
지운 것만 줄로 찍는다 — 검사한 후보마다 한 줄이면 그대로 둔 경로들이
run 의 진짜 출력을 덮는다.

실측 (문서 30건 색인 → 21건 삭제 → 재색인):

  ingest: sweeping 21 deleted-file candidates…
    purged doc11.md
    … (21줄)
  ingest: sweep complete (checked=21 purged=21 in 134ms)

`--json` 은 세 이벤트를 ingest_progress.v1 로 내보내고, ndjson 로그에는
purge 21줄 + sweep_summary 1줄이 남는다.

이슈가 참고로 적은 `reset --orphans-only` 는 그대로 뒀다. reset 에는 진행
채널 자체가 없어 sweep 하나를 위해 배선을 새로 깔아야 하는데, #229#230 이 머지된 지금 이 경로의 문서당 비용이 약 800배 떨어져 "몇 시간
무표시" 상황이 애초에 안 나온다.

곁다리 — clippy 게이트가 붉었다:

`cargo clippy --workspace --all-targets -- -D warnings` 가 main 에서
실패하고 있었다. 툴체인이 올라가면서 새 lint 둘(`question_mark`,
`manual_assert_eq`)이 기존 코드에 걸린 것이고 각각 한 줄이다. 방치하면 안
되는 이유를 이번에 겪었다 — `kebab-parse-code` 가 먼저 실패해 뒤 크레이트가
아예 컴파일되지 않았고, 그 그늘에 PR #235 에서 내가 넣은
`unnested_or_patterns` 위반이 숨어 있었다. 셋 다 여기서 고친다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
2026-08-16 20:57:34 +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
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
34672e82cc chore: PR #223 회차 1 리뷰 반영 — release notes 사실 정정 (unsafe-libyaml / crate count / 줄수)
리뷰어가 잡은 release-doc 사실 오류 정정:
- dep −2 → dep −1: serde_yaml_ng 0.10 도 unsafe-libyaml 0.2.11 을 transitive 로
  의존(Cargo.lock 확인). 제거된 건 archived serde_yaml 하나뿐. "unsafe-libyaml
  까지 제거" 는 거짓 → 4곳(release notes ×2, Cargo.toml, HOTFIXES) 정정.
- crate 24→20 → 22→20: 이 arc(#219–#222)는 #221 에서 2 crate(kebab-embed/llm)만
  제거. 24→22 는 직전 v0.31.0 spine. cumulative 표기는 명시적 주석으로 분리.
- 누적 줄수 약 −3200 → 약 −3400(실측 git diff fd3b4ae..main = 643 ins/4057 del).

코드 변경 없음(release-facing 문서 + 버전 주석). Cargo.toml 파싱 OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-27 03:22:30 +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
c48021c332 docs(hotfix): caption 캐시 GPU 도그푸딩 evidence — b2 실엔진 검증 (22× + 비결정성 박제) 2026-06-25 02:30:28 +00:00
ed03e71a1c chore: bump version 0.30.1 → 0.31.0 + v0.31.0 release notes
척추 단순화(#214) + 임베딩 캐시 전면화(#216) + OCR/caption 캐시(#217) 누적 릴리스.
minor 트리거 = 척추의 V015 drop chat_sessions + config v4→v5 breaking 마이그레이션.
도그푸딩 evidence(HOTFIXES 2026-06-25): OCR 캐시 17 jira PNG 재인덱싱 7× + 캐시 히트
created_at 17/17 고정 / 실 v3 KB 마이그레이션(config v5 + V14/V15 자동, chat_sessions 드롭).
2026-06-25 02:07:23 +00:00
54d361637b docs(hotfix): Phase 3 module split 기록 (lib.rs 4331→493) 2026-06-24 14:24:01 +00:00
26bc095418 docs(hotfix): spine Phase 3 — ingest stage 추출(API 6→2 + store/fingerprint/extract/chunk), 전부 재인덱싱 게이트 IDENTICAL 2026-06-24 13:59:56 +00:00
38990b4d44 docs(hotfix): spine Phase 2 — config 슬라이스 + 표면 정리 완료 (OCR통합 v5 + env 75→22 + consumer 슬라이스) 2026-06-24 12:00:24 +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
f54e97d2dd docs(hotfix): spine Phase 1 — 5건 삭제 완료, 코어 출력 byte-identical 검증 2026-06-24 10:22:46 +00:00
e1edb9224b docs: Task 0 parity baseline 동결 + gate를 python3/실측 경로로 수정 (output-equality 검증됨) 2026-06-24 09:00:38 +00:00
1f6c57381f docs(rag): rag-v4 LLM-judge 도그푸딩 evidence — competing 66, override 0/0, 무해
R9700 GPU ollama(gemma3:4b + snowflake-arctic-embed2 @ .244)로 rag-v3 vs rag-v4
답변 비교. 타깃 실패 모드(저신뢰 jira 가 권위 wiki 를 wiki 인용 없이 덮어씀)는
두 버전 모두 0/34 — 이 모델/코퍼스에서 재현 안 됨. 품질 지표 전부 표본오차 내 구분
불가(wiki-grounded 32 vs 31, jira-correct 29 vs 28, 거부 4 vs 5). v4 는 무해(회귀
0) + 라벨 프롬프트 도달 확인이나 trust-steering 효과는 실패 모드 미재현으로 미입증.
유일 델타: v4 약간 더 간결(평균 인용 5.14→4.23). HOTFIXES + plan doc 검증 갱신.

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
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
727648d21d fix(config): [ingest.chunking] budget floor 검증 (validate_chunking)
`Config::from_file` 에 `validate_chunking()` 추가 — 청킹 budget 의 명백히 깨진
조합을 load 시점에 reject(`validate_sources` 와 동일 패턴, `ConfigInvalid`):

- `target_tokens ≥ 16` (`MIN_CHUNK_TOKENS`)
- `overlap_tokens < target_tokens`
- `max_chunk_tokens ≥ target_tokens`

동기: md-heading-v2(PR #209)의 `max_chunk_tokens` 는 검증이 없어 `0` 같은
오설정이 청커 내부 `budget.max(1)` 클램프에 흡수돼 3-byte 청크 폭주(인덱스
bloat, 무에러)를 냈다 — reviewer 지적. 기존 `target_tokens`/`overlap_tokens`
도 미검증이라 세 필드를 한 번에 floor + 상호 제약으로 막는다.

valid config 무영향(동작·결과 불변), 깨진 config 만 명확한 메시지로 load 실패.
새 config 키·migration·동작 변경 없음 → patch-level. 테스트 6종(defaults pass +
reject 4 + e2e from_file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 02:12:03 +00:00
58386c4445 feat(chunk): md-heading-v2 — 예산 초과 청크 일반 분할 (oversize-chunk split)
v1 의 "블록 미분할" 한계를 일반화. 거대 list/code/table/paragraph 가 한 청크로
임베더 컨텍스트를 초과해 임베딩이 통째로 실패하던 문제를 해소한다. v2 는 v1 과
모든 출력이 동일하되, 청크의 실제 임베드 text 크기(`text.len()/3`)가
`max_chunk_tokens`(신규 config, byte/3, default 4000)를 넘는 청크만 줄(`\n`)
경계로, 단일 거대 줄은 UTF-8 char 경계로 잘라 각 조각이 예산 이하가 되게 한다.

- 판정 기준은 저장 `token_estimate` 가 아니라 실제 `text` 길이: ImageRef/AudioRef
  청크는 image-only 규약으로 token_estimate=0 이지만 OCR/caption text 는 클 수
  있다(빽빽한 스크린샷). 도그푸딩 일치 재테스트에서 이 image-OCR 구멍 발견·수정.
- 분할 조각 chunk_id 는 동일 block_ids 를 공유하므로 id-input 해시에 `#seg{i}`
  접미사로 충돌 회피(저장 policy_hash 는 bare — pdf-page-v1 의 `#L` 레시피 동형).
- `max_chunk_tokens` 는 v2 의 policy_hash 에만 fold(공유 ChunkPolicy 미변경 →
  코드/PDF 청커 cascade 무영향). 값 변경 시 markdown 만 재청크.
- 미분할 청크는 v1 과 byte-identical. `chunker_version` v1→v2 → 다음 plain
  `kebab ingest` 에서 markdown 1회 자동 재청크(--force 불필요, 코드/PDF 무영향).

동기: strict 임베더(AMD Lemonade `/api/embed`)는 oversize 입력을 truncate 아닌
거부(`500 too large`) — ollama 가 조용히 truncate 하던 걸 청커가 애초에 안 만들게.

검증(실험 KB, arctic-embed-l-v2 @ Lemonade): v2 전 620 중 2 doc(거대 jira list
블록) 임베드 실패 → v2 후 620/620 errors=0, 7114 청크 전부 ≤ 4000. 사용자 실
config 일치 재테스트(이미지 OCR + PDF OCR paddle-onnx ON)에서 image-OCR 구멍
발견·수정 후 dense 이미지 OCR 텍스트가 budget 초과 시 분할(token_estimate=0 →
1청크였던 것이 실제 text 기준 다중 청크로) 실증. frozen 설계 doc / frozen p1-5
spec 미변경(설계 §9 가 md-heading-v2 라벨 bump 를 변경 메커니즘으로 명시).

known limitation: PDF 는 별도 청커 pdf-page-v1.1 이라 이 split 미적용(후속 후보).

Cargo.toml 0.29.0 → 0.30.0 (신규 config 키 + 청커 동작 변경 = pre-1.0 minor +
도그푸딩 트리거). docs cascade: HOTFIXES / release-notes-v0.30.0-draft /
plan(2026-06-24) / normalize-chunk README / README / SMOKE / HANDOFF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Mc6W1fgsrbFKTsqA6P8La
2026-06-24 01:10:53 +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
e7b58017fd docs(config): v3 재편 도그푸딩 evidence + release notes
도그푸딩(release 빌드): 사용자 실제 v2 config 변환(값·주석 보존·멱등) +
재색인 0 실증(v2 자동변환·v3 디스크 양 경로 unchanged). v0.28.0 release notes
draft(변경/trade-off/mitigation/upgrade 4단락).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:12:01 +00:00
90812e981f docs(config): v3 재편 surface 동기화 + minor version bump 0.27.0→0.28.0
README Configuration([ingest.*] 레이아웃 + migrate 안내), SMOKE config 예시,
HOTFIXES dated entry(rename 매핑 + 3 불변식), 선행 마이그레이션 spec 교차링크.
인터페이스 변경(config 레이아웃 rename + env 추가) = minor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 13:03:07 +00:00
375a0693e4 chore(ocr): T11/T12 — clippy clean + docs + v0.27.0 bump
T11: fix 12 clippy lints in paddle_onnx.rs/paddle_e2e.rs (doc overindent,
finish_non_exhaustive, map_or_else, RangeInclusive::contains, cast_lossless,
is_some_and, usize::from). Full-workspace clippy -D warnings = 0.

Smoke (paddle-onnx, real binary): clean_paragraph OCR verbatim-correct, real
per-region confidence (0.99/0.96/0.95), FTS5 lexical hit on Korean(검색)+
English(embedding), parser_version folds |ocr:1:paddle-onnx:<ver>. Big page
<4s inference (5.6s ingest incl. one-time session load).

T12: README [image.ocr].engine + ARCHITECTURE OCR row + SMOKE paddle-onnx config
+ HANDOFF + HOTFIXES dated entry. Workspace version 0.26.2 → 0.27.0 (minor:
new engine value + config keys). .gitattributes: onnx as plain blobs (no git-lfs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:36:19 +00:00
47ef6532f7 chore(release): v0.26.2 — ingest 설정 변경 자동 재색인 + 문서
- Cargo.toml workspace version 0.26.1 → 0.26.2 (+Cargo.lock cascade).
  결과 포맷·CLI·wire 불변(내부 skip 판정 정정) → patch (CLAUDE.md §Versioning).
- tasks/HOTFIXES.md dated entry: 일반화 + 업그레이드 1회 재색인 안내 + 도그푸딩 evidence.
- HANDOFF.md 1줄.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:14:23 +00:00
6c9c8df43e chore(version): 0.27.0 → 0.26.1 — 새 bump 규칙상 patch
진행 로그 개선은 검색·색인 결과 불변 + 새 명령/플래그/config 없음 + additive-only
wire(asset_phase)라 CLAUDE.md 신규 규칙(기능/인터페이스 변경=minor, 없으면 patch)상
patch 가 맞음. version·라벨·HOTFIXES 헤더를 0.26.1 로 정정.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 11:02:16 +00:00
aeaa18a564 feat(ingest): 진행 로그 개선 — 파일명/phase/heartbeat/slowest 요약
OCR/caption 켜진 볼트 ingest 가 중간부터 느릴 때 TTY 진행바가 파일명·phase·
모델·경과시간을 안 보여 "멈춤"처럼 보이던 문제 해결.
- 신규 wire AssetPhase{idx,total,phase,model} + AssetTimings.ocr_ms/caption_ms
  (additive, ingest_progress.v1 유지)
- app: apply_ocr/apply_caption/embed 진입 시 AssetPhase emit + ocr/caption 시간 측정
- cli: TTY 진행바에 현재 파일명 + phase(model) + asset 경과초(heartbeat),
  종료 시 최장 소요 파일 top-5 요약(quiet 여도 출력, --json 미출력)
- wire schema / README / HANDOFF / HOTFIXES 동기화, version 0.26.0 → 0.27.0

검증(리더): clippy 0, kebab-app/cli 61그룹·parse-image/tui 14그룹 0실패(-j8).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:52:26 +00:00
8dee610a97 docs(hotfixes): arctic 종단 도그푸딩 evidence (recall@10 130/132)
kebab v0.26.0 실제 파이프라인(ollama arctic)으로 namu 재색인 → 확장 골든 eval
recall@10 130/132·recall@50 132/132·fully_consistent 22/24 종단 재현. 측정→구현
→실파이프라인 삼중 확인. 릴리스 전 도그푸딩 trigger(embedder 모델 변경) 충족.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 07:19:19 +00:00
16ddb1dfc3 docs: arctic 임베더 문서 동기화 (README/ARCHITECTURE/HANDOFF/HOTFIXES)
README Configuration: provider candle/ollama + arctic 모델(candle CLS / ollama 태그)
+ endpoint + e5→arctic cascade 경고. ARCHITECTURE: 백엔드 그래프 노드(embedollama)
+ 임베딩 백엔드 결정표(채택 근거 측정 recall@10 130) + 디렉토리 트리. HANDOFF 1줄.
HOTFIXES 2026-06-03 arctic dated entry(레지스트리/pooling/prefix/cascade + 수동
cosine 0.999984 실측 결과).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:59:23 +00:00
fc5103642e docs: 별칭 제거 문서 동기화 + version 0.25.0
HOTFIXES 2026-06-03 dated entry, 2026-05-30 design spec 제거 banner,
HANDOFF 1줄, README(별칭 섹션/config/명령표 정리), ARCHITECTURE(결정 표 +
디렉토리 트리), SMOKE/DOGFOOD config-migrate 예시 정정. workspace version
0.24.0 → 0.25.0 (+ Cargo.lock).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 21:37:58 +00:00
8bfa4ba76e fix(ingest-progress): 리뷰 반영 — store_ms 경계 정정 + 중복 expansion 프레임 가드
- store_ms 에서 stale-vector orphan purge(LanceDB I/O) 제거 → embed/vector phase
  (embed_ms)로 이동. store_ms 가 이제 SQLite put_* 만 의미(진단 정확도; 편집
  재색인 시 920ms 오귀속 제거). purge 는 여전히 unconditional + upsert 이전.
- 최종 expansion_progress 프레임을 done != last_done 로 가드 (throttle 배수 시
  중복 프레임 + chunks==0 시 0/0 프레임 제거).
- schema/HOTFIXES: store_ms/embed_ms 설명 정정 + dangling IMPL_REPORT 참조 제거.

clippy -D warnings 0, test 312 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:49:02 +00:00
a48b055358 feat(ingest): asset 내부 phase 진행 로깅 (asset_chunked/expansion_progress/asset_timings) + v0.24.0
asset(문서) 단위뿐이던 ingest 진행 이벤트에 문서 내부 phase 가시성을 추가.
큰 문서가 expansion(별칭 LLM, 청크당 순차)으로 수십 분 걸려도 진행바가
1/N 에 멈춘 듯 보이던 문제 해결.

wire ingest_progress.v1 additive (backward-compat):
- asset_chunked {idx,total,chunks} — 청킹 직후, markdown/image/pdf 전 경로
- expansion_progress {idx,total,done,chunks} — expansion 루프 스로틀
  (25청크 또는 1s, 종료 시 done==chunks). 캐시 히트도 done 에 포함
- asset_timings {idx,total,parse_ms,chunk_ms,expansion_ms,embed_ms,store_ms}
  — markdown 경로 phase별 wall-clock

설계: timing 은 kebab_core::IngestItem(wire-stable) 변경을 피해 신규
AssetTimings 이벤트로 ingest_one_asset 가 직접 emit (AssetFinished 무변경).

CLI(progress.rs): 진행바 sub-message(→ N chunks / 별칭 확장 done/chunks) +
asset 종료 시 phase timing 한 줄(fmt_ms). TUI reducer no-op arm.

검증: clippy -D warnings exit 0; cargo test -p kebab-app -p kebab-cli
312 passed/0 failed. ordering-invariant 테스트 재작성 + 신규 직렬화 테스트.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 13:58:27 +00:00
581e1d5d55 feat(cli): ingest 시 임베딩 백엔드/디바이스 한 줄 표시 + README KB 이전 문서 (v0.23.1)
- kebab-cli ingest: 시작 시 `임베딩 백엔드: <provider> (Metal/GPU 빌드|CPU) · 모델 …`
  를 stderr 로 표시 (--json/--quiet 억제). Metal 표기는 cfg!(feature=embed_metal)
  기반; 확정 런타임 디바이스는 kb.log(`candle device = …`).
- README: '외부 계산 + 로컬 검색' 절에 복사 대상(kebab.sqlite/sqlite, lancedb/vector_dir)
  + [storage] config 키 + models/assets 복사 불필요 + 동일 버전/모델 조건 + rsync 예시.
- 버전 0.23.0 → 0.23.1 (CLI 출력 + 문서만, 동작/schema 불변).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:25:45 +00:00
369aeb3d24 feat(embed): candle Metal (Apple Silicon GPU) opt-in build feature + v0.23.0
- kebab-embed-candle: `metal` feature → candle metal backend; select_device()
  picks Device::new_metal(0) (CPU fallback) under the feature, else Device::Cpu.
  .contiguous() before to_vec2 (Metal rejects strided views; CPU tolerates).
- feature passthrough: kebab-app/embed_metal → kebab-cli/embed_metal.
  Build on macOS: cargo build --release --features embed_metal.
- default (non-metal) path unchanged: clippy 0, candle units + thread_cap + parity pass.
- README + HOTFIXES: Mac-GPU-ingest → copy sqlite+lancedb → server CPU-query workflow.
- version 0.22.0 → 0.23.0 (opt-in build surface).

macOS-only compile; Metal execution/speed/parity validated by user on M4 Pro
(not buildable on the Linux CI/dev machine).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:37:08 +00:00
d85d7348a5 docs(embed-candle): 도그푸딩 + A1 반증 + MKL 부정결과 증거 기록
- HOTFIXES + release-notes: candle 전체 도그푸딩 997 docs/23,151 chunks/에러 0 (9.5h)
- A1(taskset -c 0-3) 실서버 반증: 4코어 제한에도 onnxruntime segfault → candle 만이 실 해법
- MKL 가속 부정 결과: 코어 더 쓰나 38~50% 느림 → 미채택, 순수-Rust 유지
- 패리티 2.01e-7 재확인, 성능 트레이드오프 명시

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:08:12 +00:00
6ec4e6809f fix(embed-candle): address round-1 review
- commit track-spec + meta-spec/plan into branch (HIGH: dangling `amends:` ref)
- inline parity evidence (cosine 1.0, max_abs_diff 2.01e-7) into HOTFIXES +
  release notes; drop refs to deleted IMPL_REPORT/SPIKE_REPORT (MEDIUM)
- model guard: reject non-e5-large `model` before the 2GB download so
  model_id() can't mislabel vectors (MEDIUM) + unit test
- parity test now covers BOTH query: and passage: prefixes (MEDIUM)
- guard encodings.first() index; document zero-attention/pooling invariant;
  clarify embed_batch prefixing doc (LOW)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:54:20 +00:00
8f7b6ee538 feat(embed): candle 임베딩 provider (NUMA-안전, opt-in) + v0.22.0
duo-socket NUMA 서버에서 fastembed(onnxruntime)가 intra-op 스레드를 48개로
하드코딩해 NUMA 힙 손상 → double-free 로 ingest 가 죽는 문제를 회피하기 위해,
같은 multilingual-e5-large 모델을 순수 Rust(candle)로 돌리는 opt-in 임베딩
provider 를 추가한다.

- 신규 crate kebab-embed-candle: CandleEmbedder (kebab_core::Embedder).
  hf-hub safetensors → XLMRobertaModel forward → mask mean-pool → L2 → e5
  prefix. candle 의존성 트리를 이 crate 에 격리 (core/config 외 kebab-* 의존 0).
- 스레드 캡: [models.embedding].num_threads + env KEBAB_EMBED_THREADS →
  글로벌 rayon 풀 1회 캡 (NUMA-안전 레버).
- kebab-app::embedder() 가 provider 분기 (fastembed/onnx/"" → 기존 경로 불변,
  candle → CandleEmbedder, 미지값 → 에러).
- Phase 0 스파이크 crate 제거 (production 흡수).
- 버전 0.21.1 → 0.22.0 (신규 config surface, pre-1.0 minor bump).

패리티: cosine_min=1.000000, max abs diff=2.01e-7 (< 1e-5) → embedding_version
유지, 재색인 0. fastembed default 동작/벡터 불변. wire schema 변경 없음.

검증(파일+exit code): clippy -D warnings EXIT=0(warning 0), test EXIT=0
(candle unit 5 + thread_cap rayon=4 + config 68), parity #[ignore] EXIT=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 14:52:25 +00:00
9501edd82b docs: config migrate surface 동기화 (README/HOTFIXES/HANDOFF)
README Configuration 에 kebab config migrate 불릿, HOTFIXES 에 dated entry
(메커니즘 + 도그푸딩 evidence 표 + 한계), HANDOFF 한 줄. lib.rs 백업 경로는
with_extension 유지(리뷰 nit: .toml config 엔 정상 동작, 회귀 위험 회피).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 13:25:42 +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
ca8c83b1ba chore(hotfixes): PR #192 회차 1 리뷰 반영 — refusal marker 표기 정정
`<REFUSE>` marker → citation marker(`[#번호]`) 유무 기반 (pipeline.rs:463-486).
release-notes 정정과 일관.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 05:22:57 +00:00
16c4579399 docs(hotfixes): add 2026-05-29 v0.20.2 dogfood findings + 검색 품질 baseline
8-finding 도그푸딩 라운드 및 검색 품질 baseline 결과를 HOTFIXES 에 기록.

- 8 findings 요약 표 (rag-v3, bulk schema, list docs, index_version 등)
- Finding O-2 known limitation (소형 모델 refusal 언어 불일치)
- 검색 품질 baseline 표 (hybrid MRR=0.833, lexical MRR=0.7)
- golden 큐레이션 교훈 (dispatch.py 정답 정정 → hit@3 0.9→1.0)
- eval logs cross-link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 04:54:11 +00:00
9f2a56d091 docs(hotfixes): large-scale KnowledgeBase dogfood evidence (N-gram supplement)
사용자 실제 /home/altair823/KnowledgeBase/ (1781 markdown / 9050 chunk)
를 v0.20.1+N-gram supplement 포함 binary 로 backfill 재실행:

- Backfill duration: 26.6초 (9050 chunk, OnceLock 캐시 + 1000-row
  batch transaction). ~3 ms/chunk amortized.
- '한국' query: V007 의 0 hit → V009 + N-gram 의 10 hit (Bug #8
  functional closure 실측 검증).
- '한국어' query: 5 → 10 hit (morpheme + N-gram 동시 매칭).
- 영어 whole-token: 'token'/'pipeline'/'config' = 10 hit each
  (V009 회귀 측면 정상).

Snippet evidence: KB 의 testdata/coding-md-corpus/*/...md 의
"문서를 한국어로 다시 정리하기" 패턴이 ko-dic 분해 + N-gram window
로 '한국' query 매칭 demonstrate.

기타 한국어 (서울, 지하철, 대한민국 등) 0 hit 는 KB corpus 의
단어 자체 부재 — data limitation, V009 implementation limitation X.

Test data 위치:
- /home/altair823/KnowledgeBase/ (사용자 실제 KB, 1781 markdown)
- /build/cache/tmp/v0.20.1-dogfood/kb/ (ingested SQLite + LanceDB)
- /build/cache/tmp/v0.20.1-dogfood2/corpus/ (한국어 wiki fixture)
- /build/cache/tmp/v0.20.1-v007strict/corpus/no-space.md (whitespace-less)
- /build/cache/tmp/v0.20.1-ngram/corpus/extra.md (대한민국, 한국정부, 주민등록번호)

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §9 + Appendix B
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (dogfood evidence final)
2026-05-28 14:02:02 +00:00
fe20be8195 feat(chunk): N-gram supplement (Option β) — sub-token emit for Korean compounds
#4 (사용자 요청): spec §6.2 의 Option β (sub-token 추가 emit) 를
v0.21.x P9 follow-up 에서 v0.20.1 implementation 으로 promote.
dogfood 의 ko-dic compound noun limitation (`대한민국`, `한국정부`,
`주민등록번호` 등 단일 token 정책) 해소.

Implementation (`crates/kebab-chunk/src/lib.rs::tokenize_korean_morphological`):
- 신규 helper `is_hangul()` — 한글 음절 (U+AC00..D7A3) + 자모
  (U+1100..11FF, U+3130..318F) 판정.
- lindera output 의 각 morpheme 에 대해, 한글만 + 길이 ≥ 3 인 경우
  sliding window 2-gram 추가 emit. `[한국정부, 한국, 국정, 정부]`
  형태로 token list expand.
- 영어 / 숫자 / 혼합 token 은 supplement X (false positive 회피).

Tests (`crates/kebab-chunk/tests/tokenize_korean.rs`):
- `tokenize_korean_morphological_emits_2gram_for_long_morpheme`: 5 probe
  fixture 중 supplement 발화 case 확인 (실측 `서울특별시` →
  `[서울, 특별시, 특별, 별시]`, `대한민국` → `[대한민국, 대한,
  한민, 민국]`).
- `tokenize_korean_morphological_no_2gram_for_english`: Rust optimization
  fixture 에서 영어 substring (`Rus`, `ust`, `imi`) emit 없음 보장.

Dogfood evidence (`tasks/HOTFIXES.md` 2026-05-28 entry 보강):
- '대한', '한민', '민국' query 모두 hit (대한민국 의 sliding window).
- '특별', '주민', '등록' 같은 sub-token query hit.
- 영어 'tokenizer' query 는 corpus 부재로 0 hit (supplement X).
- Trade-off: DB size +20-30% (Korean-heavy), false positive 작은 risk.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §6.2 (Option β promote)
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (post-implementation enhancement)
2026-05-28 13:48:05 +00:00
a3513c9110 docs(hotfixes): V009 dogfood verification evidence (2026-05-28)
V009 한국어 morphological tokenizer 의 dogfood 검증 결과를 HOTFIXES
2026-05-28 entry 에 보강. 14 scenario 의 hit count + ko-dic 의
compound noun 분해 evidence (서울특별시 → [서울, 특별시]) + Option α
acceptance 의 known limitation 명시.

Reference corpus: DOGFOOD.md §2.1bis 의 korea-overview.md +
korea-compound.md (10 KB 합계, 2 markdown). KB ingest + 14 query
검증 모두 expected.

사용자 KnowledgeBase 같은 영어/code 중심 KB 에서 한국어 lexical
0-hit 가 정상임을 reference fixture evidence 와 분리해 사용자
오인 방지.

Spec: docs/superpowers/specs/2026-05-28-v0.20.x-korean-morphological-tokenizer-spec.md §9
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md (S11 + dogfood evidence)
2026-05-28 13:24:29 +00:00
5d9ea588ed docs(v0.20.1): polish PR-review findings (README/HOTFIXES/schema/SKILL)
opus PR-level final review (Approved with notes) 의 4 minor finding
mechanical 정정:

1. README.md — `kebab search` row 의 영어 substring 매칭 표현이
   V007 시절 그대로였음. V009 의 whole-token 회귀 (substring → V002
   동작) 를 정직히 명시 + vector/hybrid mode 권장 안내.
2. tasks/HOTFIXES.md — 2026-05-28 entry 의 file path 정정. lexical.rs
   는 lindera 호출자가 아니라 build_match_string 의 MIN_QUERY_CHARS
   3→2 갱신만; lindera helper 의 실제 owner 는 kebab-chunk/src/lib.rs.
   ingest.rs 는 본 PR scope 외, eager backfill hook 위치는 kebab-app/
   src/app.rs::App::open_with_config.
3. docs/wire-schema/v1/search_response.schema.json — `hint` field
   description 이 V007 trigram 3-char minimum 시절 advisory 시그니처
   그대로. v0.20.1 에서 helper retired + always-omit 사실 명시
   (forward-compat 차원에서 field 만 schema 에 보존).
4. integrations/claude-code/kebab/SKILL.md — `hint` field 설명의
   self-contradiction ("present only with trigram in edge cases" vs
   "Korean 2-char now supported") 해소. retired + reuse 가능 명시.

PR-level reviewer recommendation: "Merge as-is — block 사유 아님 (모든
finding minor)". 본 commit 은 reviewer 의 옵션 1 (별 docs hotfix
commit) 채택.

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 (PR-level finding follow-up)
2026-05-28 12:53:00 +00:00
d13eb87401 docs(v0.20.x): sync README + HANDOFF + ARCH + SKILL + HOTFIXES for V009
V009 한국어 morphological tokenizer 의 사용자 visible surface 변경 +
release notes scope 를 5 docs 에 cascade.

- README.md: kebab search 명령 row 에 한국어 2자 query 지원 명시.
- integrations/claude-code/kebab/SKILL.md: V007 3-char hint 제거 +
  V009 2자 한국어 query 지원 1줄.
- HANDOFF.md: C task status 완료 flip + v0.20.1 release notes scope
  에 본 변경 추가 + 머지 후 발견 summary 행.
- docs/ARCHITECTURE.md: embedding upgrade (e5-small → e5-large),
  lindera-ko-dic FTS5 한국어 지원, version notes 추가.
- tasks/HOTFIXES.md: 2026-05-28 entry — Bug #8 V009 해소, lindera-ko-dic
  실제 crate name (spec deviation), cargo-deny deferred, Path A
  영어 substring 회귀 명시.

Spec: tasks/p9/p9-9-v0.20.x-korean-morphological-tokenizer-spec.md §7.4
Plan: docs/superpowers/plans/2026-05-28-v0.20.x-korean-morphological-tokenizer-plan.md

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-05-28 11:55:25 +00:00