fix(dogfood): document-centric try_skip_unchanged for twin-file idempotency #146
Reference in New Issue
Block a user
Delete Branch "fix/dogfood-bug4-idempotent-twin-files"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
Multi-root dogfooding (kebab-docs + httpx + zod + lodash) 의 3번째 발견 production bug fix. 1A-1 origin 의 잠재 데이터 일관성 이슈.
버그
Twin files (동일 content / 다른 path) 가 매 idempotent re-ingest 마다
Updated로 분류됨. Dogfood: 726 docs 중 27 이 twin-file victim (예: 빈__init__.py×3,AGENTS.md↔CLAUDE.md동일 content, 로고 PDF/JPG 중복).진단
assets.asset_id= blake3 content hash (PRIMARY KEY).asset_id→ 한 asset row 공유.ON CONFLICT(asset_id) DO UPDATE SET workspace_path = excluded.workspace_path→ file2 ingest 시 asset row 의workspace_path가 file1 → file2 로 덮어씀.try_skip_unchanged가get_asset_by_workspace_path(file1)→None반환 (asset row 의 path 는 file2) → unchanged 판정 실패 → re-process →Updated.Fix
try_skip_unchanged를 document-centric lookup 으로 전환:documents.workspace_path는 V001 부터 이미UNIQUE.id_for_doc(path, asset_id, parser_version)이 path 포함 → twin files 각자 별도 doc row.DocumentStore::get_document_by_workspace_pathtrait method 추가 → asset-side flip-flop 무관.비교 순서:
get_document_by_workspace_path(path)(없으면 None → re-process)doc.source_asset_id == asset.asset_id(content 변경 시 None)doc.parser_version == current_parser_version(이전 implicit 으로 id_for_doc 통해 확인, 이제 explicit)Schema migration 불필요 —
documents.workspace_pathUNIQUE 인덱스 이미 존재.변경
crates/kebab-core/src/traits.rs:DocumentStoretrait 에get_document_by_workspace_path메서드 추가crates/kebab-store-sqlite/src/documents.rs: 구현 (기존get_document의 SELECT +document_row_from_sql재사용,WHERE절만 변경 — drift 없음)crates/kebab-app/src/lib.rstry_skip_unchanged: 새 lookup 사용, 명시적 parser_version 체크 추가crates/kebab-app/tests/twin_files_idempotent.rs: 새 통합 테스트 (pkg_a/__init__.py+pkg_b/__init__.py동일 content; 첫 ingest 2 New, 두번째 ingest 0/0/2 Unchanged)검증
cargo test -p kebab-core --lib→ 57/57cargo test -p kebab-store-sqlite --lib→ 20/20cargo test -p kebab-app --test twin_files_idempotent→ 1/1 newcargo test -p kebab-app --test code_ingest_smoke→ 6/6 (regression 없음)cargo test -p kebab-app --lib→ 51/51 (regression 없음)cargo clippy -p kebab-core -p kebab-store-sqlite -p kebab-app --all-targets -- -D warningsclean영향
purge_orphan_at_workspace_path와 path-move 시나리오 영향 없음 (put_asset가 upstream 에서 처리).남은 minor issue
workspace_path컬럼이 twin files 사이에서 flip-flop 함 —try_skip_unchanged는 이제 무관하지만 asset.workspace_path 의 의미가 모호. 별도 의미적 cleanup 가치 있음 (필요시 후속 PR).🤖 Generated with Claude Code
회차 1 — APPROVE (수정 권고 1건: 함수 doc-comment 업데이트 누락)
검증 결과
cargo test -p kebab-app --test twin_files_idempotentcargo test -p kebab-app --test code_ingest_smokecargo test -p kebab-app --libcargo test -p kebab-store-sqlite --libcargo test -p kebab-core --libcargo clippy -p kebab-core -p kebab-store-sqlite -p kebab-app --all-targets -- -D warnings정확성 확인
Twin-file 시나리오:
get_document_by_workspace_path(path1)→ doc1 반환.doc1.source_asset_id == new_asset_id✅ (두 twin이 같은 asset_id를 갖더라도 각자 stable doc row 보유).get_document_by_workspace_path(path2)→ doc2 반환. 각각 Unchanged. 정확합니다.Content 변경 시나리오: 동일 경로, 다른 content →
asset.asset_id변경 →existing_doc.source_asset_id != asset.asset_id→Ok(None)→ re-process. 정확합니다.Parser/chunker/embedder version bump 시나리오: 각각 explicit check (item 2/3/4) 로 커버됩니다. 이전 코드의
id_for_doc기반 implicit 판정보다 더 self-documenting합니다.신규 파일 (no doc row):
get_document_by_workspace_path→Ok(None)→ re-process. 정확합니다.force_reingest: fn 진입 직후 첫 번째 가드로 유지. 변경 없음. ✅get_document_by_workspace_pathvsget_documentdrift 점검: SELECT 컬럼 목록,document_row_from_sql호출, blocks query, serde 역직렬화,CanonicalDocument재구성 — 모두 동일.WHERE절만doc_id = ?→workspace_path = ?로 바뀌었고,row.doc_id.clone()이 blocks query용으로 추가된 것이 유일한 차이입니다. drift 없음. ✅중요 이슈 1건 (수정 권고)
try_skip_unchanged함수 doc-comment가 구 알고리즘을 서술하고 있습니다.crates/kebab-app/src/lib.rs의 함수 바로 위 doc-comment (이 PR에서 직접 변경되지 않은 부분):이 구현은 이제
assetsrow를 조회하지 않고,id_for_doc도 사용하지 않습니다. doc-comment가 코드 동작과 다르면 다음 독자(또는 본인)가 기존 알���리즘 기준으로 디버깅하다 혼선을 겪을 수 있습니다.권장 수정: item 2/3을 새 알고리즘(doc-centric lookup,
source_asset_id비교, explicitparser_versioncheck)으로 업데이트.총평
근본 원인 진단, fix 범위, 테스트 커버리지 모두 우수합니다. doc-comment 업데이트만 추가 커밋으로 반영해 주시면 머지 가능합니다.
@@ -289,0 +308,4 @@.map_err(StoreError::from)?;let Some(row) = row else { return Ok(None) };let doc_id = kebab_core::DocumentId(row.doc_id.clone());[Minor — 선호도]
get_document(기존)은 블록 루프에서 외부row를 shadow하는 반면 이 구현은block_row로 명명해 shadow를 회피했습니다 — 더 명확합니다.row.doc_id.clone()이 필요한 이유가 코드 맥락만으로 즉시 드러나지 않을 수 있습니다. 짧은 주석 한 줄(// clone: row.doc_id is moved into params![] below)을 추가하면 다음 독자가 borrow-move 이유를 즉시 이해할 수 있습니다. 필수 사항은 아닙니다.회차 2 — doc-comment 수정 정확, 컴파일 클린, APPROVE.
360f825단일 커밋:try_skip_unchanged함수 수준 doc-comment를 round 1 지적에 따라 재작성.수정 내용 검증
get_document_by_workspace_path): "A document already exists at thisworkspace_path(get_document_by_workspace_path)" — 실제 코드 lib.rs:788-802의 호출과 정확히 일치. asset-side가 아닌 document-side lookup임과 twin-file flip-flop 원인(assets UPSERT 경쟁) 및documents.workspace_pathUNIQUE 보장까지 명확히 서술.source_asset_idcontent 비교): "existing doc'ssource_asset_idequals the freshly-scanned asset's blake3 checksum" — lib.rs:806existing_doc.source_asset_id != asset.asset_id와 정확히 대응.AssetId가 blake3 content hash임을kebab-core/src/ids.rsline 69에서 재확인.chunker_versionandlast_embedding_versionchecks immediately below" — lib.rs:817-823의 chunker/embedder check로 full cascade 완성. design §9 cross-reference 유지.tasks/HOTFIXES.md교차 참조 +documents.workspace_pathUNIQUE vsassetsflip-flop 비대칭 구조 명확히 기술.기타
cargo check -p kebab-app클린 (24초, dev profile). 이 커밋은 doc-comment만 수정이며 round 1에서 이미 검증된 구현 코드는 무변경 — 추가 지적 없음.