feat(v0.17.0/PR-B/B1): C typedef extractor + parser_version bump + orphan purge cascade
closure of HOTFIXES 2026-05-21. C typedef-wrapped anonymous
struct/enum/union 이 typedef alias 이름으로 symbol unit 방출.
- crates/kebab-parse-code/src/c.rs: type_definition 분기 추가.
inner anonymous struct_specifier / enum_specifier / union_specifier
탐지 → declarator field 의 type_identifier 재귀 추출 → synthetic
unit (typedef alias). named inner aggregate / plain alias 는
기존대로 glue. PARSER_VERSION code-c-v1 → code-c-v2.
recover_typedef_alias + extract_typedef_alias_name helper 추가.
- crates/kebab-store-sqlite/src/store.rs: 두 helper 신규
(parser_version bump cascade 용 doc-id 기반 orphan purge).
- stale_chunk_ids_for_workspace_path_except_doc_id(workspace_path,
keep_doc_id) — sister of stale_chunk_ids_at, doc_id 기반.
- purge_document_at_workspace_path_except_doc_id(workspace_path,
keep_doc_id) — CASCADE document/chunks 제거, assets 보존.
keep_doc_id="" 가 "모든 doc 제거" 사용.
- crates/kebab-app/src/lib.rs: try_skip_unchanged 의 parser_mismatch
분기에서 purge_workspace_path_for_parser_bump 호출. helper 가
app.vector() 로 lazy 접근 + delete_by_chunk_ids + SQLite document
row 제거. Ok(None) 반환 전 cleanup 끝나서 caller 의 새 INSERT 시
idx_docs_workspace_path UNIQUE 충돌 회피.
- tests:
- c.rs unit tests 4 신규 — typedef_struct_emits_unit /
typedef_enum_emits_unit / typedef_union_emits_unit /
typedef_to_existing_type_stays_glue (negative).
- tier1_c_ingest_searchable: parser_version assertion code-c-v1 →
code-c-v2.
- 회귀: bytes-edit 경로 (asset_id 변경) 의 기존 purge_orphan_at_workspace_path
+ purge_vector_orphans_for_workspace_path 는 그대로 — 신규 분기와
공존, 기존 test 모두 PASS.
미해결 (Risks): nested typedef (typedef struct { struct {...} inner; }
Outer;) 의 inner 익명 struct 는 여전히 glue — v2 의 1차 범위는
top-level typedef alias 만.
cargo test --workspace --no-fail-fast -j 1 + clippy 통과.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -464,6 +464,74 @@ impl SqliteStore {
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// v0.17.0 PR-B: sister of [`Self::stale_chunk_ids_at`] for the
|
||||
/// `parser_version` bump cascade. When `doc_id` depends on
|
||||
/// `parser_version` (design §9) and an extractor ships a new
|
||||
/// `PARSER_VERSION`, the next ingest computes a fresh `doc_id` for
|
||||
/// the *same* `(workspace_path, asset_id)` pair. The existing
|
||||
/// asset_id-keyed [`Self::stale_chunk_ids_at`] does NOT fire (same
|
||||
/// asset), so the legacy `chunks` rows and their LanceDB shadows
|
||||
/// would orphan. This helper queries by `workspace_path` instead,
|
||||
/// excluding the freshly-computed `keep_doc_id` so a re-entry
|
||||
/// during the same ingest doesn't re-sweep the new row.
|
||||
///
|
||||
/// Caller usage: pass the *new* `doc_id` if known; pass an empty
|
||||
/// string when called before the new INSERT (the case in
|
||||
/// `try_skip_unchanged`) — all existing docs at `workspace_path`
|
||||
/// are then collected as stale.
|
||||
pub fn stale_chunk_ids_for_workspace_path_except_doc_id(
|
||||
&self,
|
||||
workspace_path: &str,
|
||||
keep_doc_id: &str,
|
||||
) -> Result<Vec<kebab_core::ChunkId>> {
|
||||
let conn = self.lock_conn();
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT c.chunk_id
|
||||
FROM chunks c
|
||||
INNER JOIN documents d ON c.doc_id = d.doc_id
|
||||
WHERE d.workspace_path = ?1 AND d.doc_id != ?2",
|
||||
)
|
||||
.map_err(StoreError::from)?;
|
||||
let rows = stmt
|
||||
.query_map(params![workspace_path, keep_doc_id], |row| {
|
||||
row.get::<_, String>(0)
|
||||
})
|
||||
.map_err(StoreError::from)?;
|
||||
let mut out: Vec<kebab_core::ChunkId> = Vec::new();
|
||||
for row in rows {
|
||||
let id = row.map_err(StoreError::from)?;
|
||||
out.push(kebab_core::ChunkId(id));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// v0.17.0 PR-B: sweep the SQLite document chain (`documents` →
|
||||
/// `blocks` / `chunks` / `embedding_records` via CASCADE) for every
|
||||
/// row at `workspace_path` whose `doc_id` differs from `keep_doc_id`.
|
||||
/// Pair with [`Self::stale_chunk_ids_for_workspace_path_except_doc_id`]
|
||||
/// — caller fetches the chunk_ids first, hands them to
|
||||
/// `VectorStore::delete_by_chunk_ids`, then calls this sweep.
|
||||
/// `assets` row is preserved (same bytes, same asset_id — only the
|
||||
/// derived `doc_id` changed).
|
||||
///
|
||||
/// `keep_doc_id = ""` deletes every doc at `workspace_path`
|
||||
/// (semantics mirror the sister helper above — used by
|
||||
/// `try_skip_unchanged` before the new INSERT exists).
|
||||
pub fn purge_document_at_workspace_path_except_doc_id(
|
||||
&self,
|
||||
workspace_path: &str,
|
||||
keep_doc_id: &str,
|
||||
) -> Result<()> {
|
||||
let conn = self.lock_conn();
|
||||
conn.execute(
|
||||
"DELETE FROM documents WHERE workspace_path = ?1 AND doc_id != ?2",
|
||||
params![workspace_path, keep_doc_id],
|
||||
)
|
||||
.map_err(StoreError::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sweep stale `assets` + `documents` + downstream rows when the file
|
||||
|
||||
Reference in New Issue
Block a user