From 922849cd956dceed2458559a63493857b53be1d3 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Sat, 9 May 2026 01:01:01 +0900
Subject: [PATCH] feat(config): search.stale_threshold_days (fb-32)
default 30 days. env override KEBAB_SEARCH_STALE_THRESHOLD_DAYS.
Malformed env values are silently ignored, matching the existing
apply_env pattern.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
crates/kebab-config/src/lib.rs | 51 ++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/crates/kebab-config/src/lib.rs b/crates/kebab-config/src/lib.rs
index bbcd825..00a40ef 100644
--- a/crates/kebab-config/src/lib.rs
+++ b/crates/kebab-config/src/lib.rs
@@ -131,12 +131,21 @@ pub struct SearchCfg {
/// (corpus_revision mismatch) are evicted on next access.
#[serde(default = "default_cache_capacity")]
pub cache_capacity: usize,
+ /// p9-fb-32: hits and citations whose source doc was last
+ /// re-processed more than this many days ago are marked
+ /// `stale: true` in wire / TUI / CLI surfaces. `0` disables.
+ #[serde(default = "default_stale_threshold_days")]
+ pub stale_threshold_days: u32,
}
fn default_cache_capacity() -> usize {
256
}
+fn default_stale_threshold_days() -> u32 {
+ 30
+}
+
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RagCfg {
pub prompt_template_version: String,
@@ -317,6 +326,7 @@ impl Config {
rrf_k: 60,
snippet_chars: 220,
cache_capacity: default_cache_capacity(),
+ stale_threshold_days: 30,
},
rag: RagCfg {
prompt_template_version: "rag-v1".to_string(),
@@ -577,6 +587,11 @@ impl Config {
self.search.snippet_chars = n;
}
}
+ "KEBAB_SEARCH_STALE_THRESHOLD_DAYS" => {
+ if let Ok(n) = v.parse::() {
+ self.search.stale_threshold_days = n;
+ }
+ }
// rag
"KEBAB_RAG_PROMPT_TEMPLATE_VERSION" => {
@@ -944,6 +959,7 @@ default_k = 10
hybrid_fusion = "rrf"
rrf_k = 60
snippet_chars = 220
+stale_threshold_days = 30
[rag]
prompt_template_version = "rag-v1"
@@ -981,6 +997,41 @@ max_context_tokens = 8000
let WorkspaceCfg { root: _, exclude: _ } = &ws;
}
+ #[test]
+ fn default_stale_threshold_is_30() {
+ let c = Config::defaults();
+ assert_eq!(c.search.stale_threshold_days, 30);
+ }
+
+ #[test]
+ fn env_override_stale_threshold() {
+ let c = Config::defaults();
+ let env: HashMap = [
+ ("KEBAB_SEARCH_STALE_THRESHOLD_DAYS".to_string(), "7".to_string()),
+ ]
+ .into_iter()
+ .collect();
+ let c = c.apply_env(&env);
+ assert_eq!(c.search.stale_threshold_days, 7);
+ }
+
+ #[test]
+ fn negative_stale_threshold_rejected_at_validation() {
+ let c = Config::defaults();
+ // u32 cannot hold a negative — represent the failure path through
+ // `apply_env` parse-failure: malformed values are silently ignored
+ // (existing pattern, see KEBAB_SEARCH_DEFAULT_K). For TOML-level
+ // negative rejection we rely on serde's u32 type; assert that the
+ // env path leaves the default in place when given garbage.
+ let env: HashMap = [
+ ("KEBAB_SEARCH_STALE_THRESHOLD_DAYS".to_string(), "-5".to_string()),
+ ]
+ .into_iter()
+ .collect();
+ let c = c.apply_env(&env);
+ assert_eq!(c.search.stale_threshold_days, 30, "garbage env value must not corrupt the default");
+ }
+
#[test]
fn xdg_paths_honor_env() {
// Must restore env after the test to avoid polluting other tests.