From f3587b7143b7c52d600b9d8df84a0ab9645df800 Mon Sep 17 00:00:00 2001 From: altair823 Date: Sat, 30 May 2026 13:41:28 +0000 Subject: [PATCH] =?UTF-8?q?feat(store):=20filter=5Fchunks=20sentinel=20?= =?UTF-8?q?=EB=B3=84=EC=B9=AD=20candidate=20strip=20(committed=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LanceDB 후보의 sentinel chunk_id({orig}#alias)는 chunks JOIN 에서 탈락해 VectorRetriever strip 이전에 사라진다. candidate 를 kebab_core::strip_alias_suffix 로 원본 chunk_id 로 strip 해 IN-list/JOIN 에 넣어(committed 판정은 원본 body chunk 기준) 통과시키되, 반환은 입력 candidate 형태(sentinel 유지) — VectorRetriever 가 그 sentinel 을 받아 strip+dedup 한다. SQL replace 대신 (b) Rust strip 채택(명확). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/kebab-store-sqlite/src/filters.rs | 71 ++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/kebab-store-sqlite/src/filters.rs b/crates/kebab-store-sqlite/src/filters.rs index f2e6648..ff3b899 100644 --- a/crates/kebab-store-sqlite/src/filters.rs +++ b/crates/kebab-store-sqlite/src/filters.rs @@ -59,15 +59,25 @@ impl SqliteStore { return Ok(Vec::new()); } - // Deduplicate the IN-list so a pathological caller passing - // `[c1, c1, c1]` doesn't blow the SQL placeholder count. + // sentinel 별칭 candidate({orig}#alias)는 chunks 에 원본 chunk 가 없어 + // (chunks JOIN 실패) committed 판정을 못 받는다. 후보를 원본 chunk_id 로 + // strip 해 IN-list/JOIN 에 넣고(committed 판정은 원본 body chunk 기준), + // 통과 여부는 원본 기준으로 매핑하되 반환은 입력 candidate 형태(sentinel + // 유지) — VectorRetriever(Task 4)가 그 sentinel 을 받아 strip+dedup 한다. + // 설계 spec 2026-05-30-dense-alias-vectors-design.md §3.5-3. + // + // Deduplicate the IN-list (on the stripped original) so a + // pathological caller passing `[c1, c1, c1]` — or a body+alias + // pair `[c1, c1#alias]` that strips to the same original — + // doesn't blow the SQL placeholder count. let unique_ids: Vec = { let mut seen = HashSet::new(); chunk_ids .iter() .filter_map(|c| { - if seen.insert(c.0.as_str()) { - Some(c.0.clone()) + let orig = kebab_core::strip_alias_suffix(c.0.as_str()); + if seen.insert(orig.to_string()) { + Some(orig.to_string()) } else { None } @@ -242,7 +252,11 @@ impl SqliteStore { let mut out = Vec::with_capacity(chunk_ids.len()); for cand in chunk_ids { - let workspace_path = match allowed.get(&cand.0) { + // committed 판정은 원본 chunk 기준(allowed 는 원본 chunk_id 로 키됨). + // candidate 가 sentinel 이면 strip 한 원본으로 조회하고, 통과 시 + // 입력 candidate 형태 그대로 반환한다. + let orig = kebab_core::strip_alias_suffix(cand.0.as_str()); + let workspace_path = match allowed.get(orig) { Some(p) => p, None => continue, }; @@ -558,6 +572,53 @@ mod tests { assert_eq!(out, vec![cid(c1)]); } + #[test] + fn filter_chunks_sentinel_alias_candidate_passes_via_original() { + // 별칭 dense 벡터 sentinel candidate({orig}#alias)는 원본 chunk 가 + // committed 면 통과해야 한다(strip 해 JOIN). 반환은 입력 candidate + // 형태 그대로(sentinel 유지) — VectorRetriever 가 strip+dedup. + let tmp = TempDir::new().unwrap(); + let store = open_store(&tmp); + let c1 = "11111111111111111111111111111111"; + seed_committed( + &store, + c1, + "d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1d1", + "a.md", + "en", + &[], + "primary", + ); + + // sentinel candidate 단독 → 원본 c1 committed 라 통과, sentinel 형태 유지. + let sentinel = format!("{c1}{}", kebab_core::ALIAS_SUFFIX); + let out = store + .filter_chunks(&[cid(&sentinel)], &SearchFilters::default()) + .unwrap(); + assert_eq!( + out, + vec![cid(&sentinel)], + "sentinel candidate must pass via its committed original and be returned verbatim" + ); + + // body + sentinel 둘 다 입력 → 둘 다 통과, 입력 순서 보존. + let out = store + .filter_chunks(&[cid(c1), cid(&sentinel)], &SearchFilters::default()) + .unwrap(); + assert_eq!(out, vec![cid(c1), cid(&sentinel)]); + + // 원본이 미존재(uncommitted)면 sentinel 도 탈락. + let orphan_sentinel = + format!("99999999999999999999999999999999{}", kebab_core::ALIAS_SUFFIX); + let out = store + .filter_chunks(&[cid(&orphan_sentinel)], &SearchFilters::default()) + .unwrap(); + assert!( + out.is_empty(), + "sentinel whose original is not committed must be dropped" + ); + } + #[test] fn filter_chunks_tags_any_lang_trust_path_glob() { let tmp = TempDir::new().unwrap();