fix(dogfood): document-centric fetch_span + assets.workspace_path semantic doc #150

Merged
altair823 merged 1 commits from fix/dogfood-asset-flip-flop-cleanup into main 2026-05-20 08:08:57 +00:00
Owner

요약

Multi-root dogfooding 결과 #3 (asset table workspace_path flip-flop) 의 semantic cleanup. 실용 영향은 minor 였으나 (twin file 의 fetch span 1곳만 잠재 영향) PR #146 / #149 가 그어 둔 "document-centric lookup" 원칙을 마지막 caller 까지 일관 적용.

진단

assets.asset_id = blake3 content hash (PRIMARY KEY) → twin files (동일 content / 다른 path) 가 한 asset row 공유 → UPSERT ON CONFLICT(asset_id) DO UPDATE SET workspace_path = excluded 가 매 ingest 마다 workspace_path flip-flop.

  • PR #146 (twin idempotent): try_skip_unchanged 가 document-centric → flip-flop 무관
  • PR #149 (reset --orphans-only): document-centric → flip-flop 무관
  • 남은 1곳: fetch.rs:193::fetch_spanget_asset_by_workspace_path(&doc.workspace_path) — twin file 의 경우 잘못된 asset row 의 media_type 으로 PDF/audio 분기 판단 가능 (rare in practice, semantically incorrect)

Fix

  1. 새 trait method DocumentStore::get_asset(asset_id: &AssetId) -> Result<Option<RawAsset>> — asset_id 가 PRIMARY KEY 라 flip-flop-immune by construction.
  2. crates/kebab-store-sqlite::SqliteStore::get_asset 구현 — 기존 asset_from_row 매퍼 재사용 (drift 없음).
  3. fetch.rs::fetch_span refactorget_asset_by_workspace_path(&doc.workspace_path)get_asset(&doc.source_asset_id) 2-step lookup. 이미 fn 안에 doc 있음 (get_document_by_workspace_path 호출 후).
  4. UPSERT doc-comment 갱신 (store.rs::upsert_asset_row) — "last-registered path" semantic 명시 + 미래 reader 가 flip-flop 을 "fix" 하려고 시도하지 않도록 안내.
  5. DocumentStore::get_asset_by_workspace_path trait method 와 SqliteStore 구현은 유지 — production caller 0이지만 store-sqlite round-trip test 2건이 사용 중. 보수적 선택 (deprecation 또는 후속 제거는 별도 cleanup).

변경 (5 파일)

  • crates/kebab-core/src/traits.rs: get_asset trait method 추가 + AssetId import
  • crates/kebab-store-sqlite/src/documents.rs: get_asset 구현 (기존 asset_from_row 재사용)
  • crates/kebab-app/src/fetch.rs: fetch_span 의 lookup refactor
  • crates/kebab-store-sqlite/src/store.rs: UPSERT doc-comment 갱신 (14 줄, last-registered semantic + 2-step 패턴 가이드)
  • crates/kebab-app/tests/twin_files_fetch_span.rs: 회귀 테스트 (twin_files_fetch_span_uses_correct_asset) — 동일 content 2 md 파일 ingest → 둘 다 fetch_span 성공 → 재 ingest (flip-flop trigger) → 여전히 둘 다 성공

검증

  • cargo test -p kebab-core --lib → 57/57
  • cargo test -p kebab-store-sqlite --lib → 20/20
  • cargo test -p kebab-app --test twin_files_fetch_span → 1/1 (new)
  • cargo test -p kebab-app --test twin_files_idempotent → 1/1 (PR #146 invariant)
  • cargo test -p kebab-app --test file_deletion_auto_purge → 2/2 (PR #148 invariant)
  • cargo test -p kebab-app --test reset_orphans → 1/1 (PR #149 invariant)
  • cargo test -p kebab-app --test code_ingest_smoke → 6/6 (PR #142 invariant)
  • cargo test -p kebab-app --lib → 52/52
  • cargo clippy -p kebab-core -p kebab-store-sqlite -p kebab-app --all-targets -- -D warnings clean

영향

  • wire schema 변경 없음 (internal trait surface 만).
  • frozen design 변경 없음 — semantic doc 명시일 뿐 동작 변경 없음.
  • 기존 색인 데이터 invalidation 없음.
  • backwards-compat: get_asset_by_workspace_path 보존 (deprecation/제거는 별도).

🤖 Generated with Claude Code

## 요약 Multi-root dogfooding 결과 #3 (asset table `workspace_path` flip-flop) 의 semantic cleanup. 실용 영향은 minor 였으나 (twin file 의 `fetch span` 1곳만 잠재 영향) PR #146 / #149 가 그어 둔 "document-centric lookup" 원칙을 마지막 caller 까지 일관 적용. ## 진단 `assets.asset_id` = blake3 content hash (PRIMARY KEY) → twin files (동일 content / 다른 path) 가 한 asset row 공유 → UPSERT `ON CONFLICT(asset_id) DO UPDATE SET workspace_path = excluded` 가 매 ingest 마다 `workspace_path` flip-flop. - PR #146 (twin idempotent): `try_skip_unchanged` 가 document-centric → flip-flop 무관 ✅ - PR #149 (reset --orphans-only): document-centric → flip-flop 무관 ✅ - **남은 1곳**: `fetch.rs:193::fetch_span` 의 `get_asset_by_workspace_path(&doc.workspace_path)` — twin file 의 경우 잘못된 asset row 의 `media_type` 으로 PDF/audio 분기 판단 가능 (rare in practice, semantically incorrect) ## Fix 1. **새 trait method `DocumentStore::get_asset(asset_id: &AssetId) -> Result<Option<RawAsset>>`** — asset_id 가 PRIMARY KEY 라 flip-flop-immune by construction. 2. `crates/kebab-store-sqlite::SqliteStore::get_asset` 구현 — 기존 `asset_from_row` 매퍼 재사용 (drift 없음). 3. **`fetch.rs::fetch_span` refactor** — `get_asset_by_workspace_path(&doc.workspace_path)` → `get_asset(&doc.source_asset_id)` 2-step lookup. 이미 fn 안에 doc 있음 (`get_document_by_workspace_path` 호출 후). 4. **UPSERT doc-comment 갱신** (`store.rs::upsert_asset_row`) — "last-registered path" semantic 명시 + 미래 reader 가 flip-flop 을 "fix" 하려고 시도하지 않도록 안내. 5. `DocumentStore::get_asset_by_workspace_path` trait method 와 SqliteStore 구현은 **유지** — production caller 0이지만 store-sqlite round-trip test 2건이 사용 중. 보수적 선택 (deprecation 또는 후속 제거는 별도 cleanup). ## 변경 (5 파일) - `crates/kebab-core/src/traits.rs`: `get_asset` trait method 추가 + `AssetId` import - `crates/kebab-store-sqlite/src/documents.rs`: `get_asset` 구현 (기존 `asset_from_row` 재사용) - `crates/kebab-app/src/fetch.rs`: `fetch_span` 의 lookup refactor - `crates/kebab-store-sqlite/src/store.rs`: UPSERT doc-comment 갱신 (14 줄, last-registered semantic + 2-step 패턴 가이드) - `crates/kebab-app/tests/twin_files_fetch_span.rs`: 회귀 테스트 (`twin_files_fetch_span_uses_correct_asset`) — 동일 content 2 md 파일 ingest → 둘 다 `fetch_span` 성공 → 재 ingest (flip-flop trigger) → 여전히 둘 다 성공 ## 검증 - `cargo test -p kebab-core --lib` → 57/57 - `cargo test -p kebab-store-sqlite --lib` → 20/20 - `cargo test -p kebab-app --test twin_files_fetch_span` → 1/1 (new) - `cargo test -p kebab-app --test twin_files_idempotent` → 1/1 (PR #146 invariant) - `cargo test -p kebab-app --test file_deletion_auto_purge` → 2/2 (PR #148 invariant) - `cargo test -p kebab-app --test reset_orphans` → 1/1 (PR #149 invariant) - `cargo test -p kebab-app --test code_ingest_smoke` → 6/6 (PR #142 invariant) - `cargo test -p kebab-app --lib` → 52/52 - `cargo clippy -p kebab-core -p kebab-store-sqlite -p kebab-app --all-targets -- -D warnings` clean ## 영향 - wire schema 변경 없음 (internal trait surface 만). - frozen design 변경 없음 — semantic doc 명시일 뿐 동작 변경 없음. - 기존 색인 데이터 invalidation 없음. - backwards-compat: `get_asset_by_workspace_path` 보존 (deprecation/제거는 별도). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
altair823 added 1 commit 2026-05-20 08:04:51 +00:00
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>
claude-reviewer-01 approved these changes 2026-05-20 08:08:12 +00:00
claude-reviewer-01 left a comment
Member

회차 1 — APPROVE (선택적 제안 1건)

확인 항목

항목 결과
DocumentStore::get_asset 시그니처 (fn get_asset(&self, id: &AssetId) -> anyhow::Result<Option<RawAsset>>)
AssetId import (crate::ids::{AssetId, ChunkId, DocumentId})
SqliteStore impl: WHERE asset_id = ? + asset_from_row 재사용 (drift 불가)
fetch.rs::fetch_span: get_documentdoc.source_asset_idget_asset 2-step
UPSERT doc-comment: last-registered path semantic + "Do NOT fix the flip-flop" 경고
get_asset_by_workspace_path 보존 (trait + impl) + NOTE doc-comment 갱신
RawAsset vs kebab_core::Asset 이름 충돌 없음 (pub use asset::RawAsset만 export)

정확성

fetch_spanget_document(by doc_id)로 doc을 가져온 직후 doc.source_asset_idget_asset을 호출합니다. doc.source_asset_id는 extractor의 CanonicalDocument 생성 시점에 고정된 PK 참조라 flip-flop과 완전히 무관합니다. 기존 버그 시나리오가 정확히 차단됩니다.

테스트

  • kebab-core --lib: 57/57
  • kebab-store-sqlite --lib: 20/20
  • twin_files_fetch_span: 1/1 (새 회귀 커버리지)
  • twin_files_idempotent: 1/1
  • file_deletion_auto_purge: 2/2
  • reset_orphans: 1/1
  • code_ingest_smoke: 6/6
  • kebab-app --lib: 52/52
  • clippy (-D warnings): clean

선택 제안

get_asset_by_workspace_path production caller 0개 → #[deprecated] 어노테이션을 trait method에 붙이면 컴파일을 깨지 않고 미래 caller에게 컴파일 경고를 전달할 수 있습니다. round-trip 테스트 2건에 #[allow(deprecated)]만 추가하면 됩니다. 지금 blocking이 아닌 별도 cleanup PR 후보입니다.

회차 1 — APPROVE (선택적 제안 1건) **확인 항목** | 항목 | 결과 | |------|------| | `DocumentStore::get_asset` 시그니처 (`fn get_asset(&self, id: &AssetId) -> anyhow::Result<Option<RawAsset>>`) | ✅ | | `AssetId` import (`crate::ids::{AssetId, ChunkId, DocumentId}`) | ✅ | | SqliteStore impl: `WHERE asset_id = ?` + `asset_from_row` 재사용 (drift 불가) | ✅ | | `fetch.rs::fetch_span`: `get_document` → `doc.source_asset_id` → `get_asset` 2-step | ✅ | | UPSERT doc-comment: last-registered path semantic + "Do NOT fix the flip-flop" 경고 | ✅ | | `get_asset_by_workspace_path` 보존 (trait + impl) + NOTE doc-comment 갱신 | ✅ | | `RawAsset` vs `kebab_core::Asset` 이름 충돌 없음 (`pub use asset::RawAsset`만 export) | ✅ | **정확성** `fetch_span`은 `get_document`(by doc_id)로 `doc`을 가져온 직후 `doc.source_asset_id`로 `get_asset`을 호출합니다. `doc.source_asset_id`는 extractor의 `CanonicalDocument` 생성 시점에 고정된 PK 참조라 flip-flop과 완전히 무관합니다. 기존 버그 시나리오가 정확히 차단됩니다. **테스트** - `kebab-core --lib`: 57/57 ✅ - `kebab-store-sqlite --lib`: 20/20 ✅ - `twin_files_fetch_span`: 1/1 ✅ (새 회귀 커버리지) - `twin_files_idempotent`: 1/1 ✅ - `file_deletion_auto_purge`: 2/2 ✅ - `reset_orphans`: 1/1 ✅ - `code_ingest_smoke`: 6/6 ✅ - `kebab-app --lib`: 52/52 ✅ - clippy (`-D warnings`): clean ✅ **선택 제안** `get_asset_by_workspace_path` production caller 0개 → `#[deprecated]` 어노테이션을 trait method에 붙이면 컴파일을 깨지 않고 미래 caller에게 컴파일 경고를 전달할 수 있습니다. round-trip 테스트 2건에 `#[allow(deprecated)]`만 추가하면 됩니다. 지금 blocking이 아닌 별도 cleanup PR 후보입니다.
@@ -0,0 +152,4 @@
assert_eq!(report2.errors, 0, "no ingest errors on second run; report={report2:?}");
// Re-open app after second ingest and verify span still works on both.
let app2 = env.app();

테스트 후반부(186–215줄)에서 ingest_with_config두 번째로 호출assets.workspace_path flip-flop을 강제로 발생시킨 뒤 양쪽 twin 모두 fetch_span이 여전히 성공하는지 검증합니다. 이 재-ingest 패스가 바로 버그가 재현되던 시나리오이므로 단순 first-ingest 검증보다 훨씬 가치 있는 회귀 커버리지입니다.

테스트 후반부(186–215줄)에서 `ingest_with_config`를 **두 번째로 호출**해 `assets.workspace_path` flip-flop을 강제로 발생시킨 뒤 양쪽 twin 모두 `fetch_span`이 여전히 성공하는지 검증합니다. 이 재-ingest 패스가 바로 버그가 재현되던 시나리오이므로 단순 first-ingest 검증보다 훨씬 가치 있는 회귀 커버리지입니다.
@@ -8,7 +8,7 @@ use serde_json::Value;
use crate::asset::{RawAsset, WorkspacePath};
use crate::chunk::Chunk;

get_asset_by_workspace_path 는 production caller가 0개입니다. #[deprecated(note = "Twin-file unsafe: assets.workspace_path flip-flops. Use get_asset(&doc.source_asset_id) instead.")] 를 trait method에 붙이면 기존 round-trip 테스트 2건에 #[allow(deprecated)] 가 필요하지만, 컴파일은 유지되면서 미래 caller에게 컴파일 단계에서 경고가 전달됩니다. 지금 block은 아니고, 후속 cleanup PR 후보로만 남겨둡니다.

`get_asset_by_workspace_path` 는 production caller가 0개입니다. `#[deprecated(note = "Twin-file unsafe: assets.workspace_path flip-flops. Use get_asset(&doc.source_asset_id) instead.")]` 를 trait method에 붙이면 기존 round-trip 테스트 2건에 `#[allow(deprecated)]` 가 필요하지만, 컴파일은 유지되면서 미래 caller에게 컴파일 단계에서 경고가 전달됩니다. 지금 block은 아니고, 후속 cleanup PR 후보로만 남겨둡니다.

get_asset 구현이 기존 asset_from_row 헬퍼를 그대로 재사용하고 있어 두 쿼리 간 컬럼 목록 drift가 구조적으로 불가능한 점을 확인했습니다. doc-comment(SELECTs in get_asset and get_asset_by_workspace_path must both include all nine columns)까지 갱신돼 있어서 미래 기여자에게도 명확합니다.

`get_asset` 구현이 기존 `asset_from_row` 헬퍼를 그대로 재사용하고 있어 두 쿼리 간 컬럼 목록 drift가 구조적으로 불가능한 점을 확인했습니다. doc-comment(`SELECTs in get_asset and get_asset_by_workspace_path must both include all nine columns`)까지 갱신돼 있어서 미래 기여자에게도 명확합니다.
altair823 merged commit ce1c778b4a into main 2026-05-20 08:08:57 +00:00
altair823 deleted branch fix/dogfood-asset-flip-flop-cleanup 2026-05-20 08:08:58 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#150