From 8b0f64db6b0638a7739b31c8d1c9f8f59a172ab6 Mon Sep 17 00:00:00 2001 From: th-kim0823 Date: Sat, 9 May 2026 00:52:46 +0900 Subject: [PATCH] feat(core): SearchHit gains indexed_at + stale (fb-32) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domain field additions for p9-fb-32. Wire serialization is automatic via serde rfc3339. Other crates fail to compile until they populate the new fields — fixed in subsequent tasks. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/kebab-core/src/search.rs | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/crates/kebab-core/src/search.rs b/crates/kebab-core/src/search.rs index 9621d61..6e49b5a 100644 --- a/crates/kebab-core/src/search.rs +++ b/crates/kebab-core/src/search.rs @@ -48,6 +48,13 @@ pub struct SearchHit { pub index_version: IndexVersion, pub embedding_model: Option, pub chunker_version: ChunkerVersion, + /// p9-fb-32: source doc's `documents.updated_at` (last actual re-process). + /// fb-23 incremental ingest skip path leaves this unchanged. + #[serde(with = "time::serde::rfc3339")] + pub indexed_at: OffsetDateTime, + /// p9-fb-32: server-computed `now - indexed_at > threshold` per + /// `config.search.stale_threshold_days`. `false` when threshold = 0. + pub stale: bool, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -88,3 +95,44 @@ pub struct DocSummary { pub parser_version: ParserVersion, pub chunker_version: ChunkerVersion, } + +#[cfg(test)] +mod tests { + use super::*; + use time::macros::datetime; + + #[test] + fn search_hit_serializes_indexed_at_and_stale() { + let hit = SearchHit { + rank: 1, + chunk_id: ChunkId("c".to_string()), + doc_id: DocumentId("d".to_string()), + doc_path: WorkspacePath::new("a/b.md".to_string()).unwrap(), + heading_path: vec!["H".to_string()], + section_label: None, + snippet: "s".to_string(), + citation: Citation::Line { + path: WorkspacePath::new("a/b.md".to_string()).unwrap(), + start: 1, + end: 1, + section: None, + }, + retrieval: RetrievalDetail { + method: SearchMode::Lexical, + fusion_score: 0.5, + lexical_score: Some(0.5), + vector_score: None, + lexical_rank: Some(1), + vector_rank: None, + }, + index_version: IndexVersion("v1".to_string()), + embedding_model: None, + chunker_version: ChunkerVersion("c1".to_string()), + indexed_at: datetime!(2026-05-09 12:00:00 UTC), + stale: true, + }; + let v = serde_json::to_value(&hit).unwrap(); + assert_eq!(v["indexed_at"], "2026-05-09T12:00:00Z"); + assert_eq!(v["stale"], true); + } +}