coding-agent-search 0.5.2

Unified TUI search over local coding agent histories
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
// Dead-code tolerated module-wide: the readiness vocabulary lands here
// ahead of wiring into status / health / capabilities / search metadata
// callers. Downstream slices will plug these types into the JSON payload
// builders in src/lib.rs and the TUI status surface.
#![allow(dead_code)]

//! Truthful readiness-state vocabulary for lexical vs. semantic search
//! (bead ibuuh.9).
//!
//! Today cass reports a single "healthy / unhealthy" bit that conflates
//! "lexical index missing" (actually broken — search returns nothing),
//! "lexical index stale but searchable" (slightly old but fully correct),
//! "lexical index rebuilding in background" (search works, new content
//! will land shortly), and "semantic tier still backfilling" (lexical
//! results are complete, hybrid refinement catches up later). Agents and
//! humans keep triggering unnecessary repair rituals because the single
//! health bit cannot distinguish these cases.
//!
//! This module lands the vocabulary that future status/capabilities/
//! search-metadata payloads will project into their JSON. The fields are
//! intentionally orthogonal — lexical readiness and semantic readiness
//! are independent dimensions, and the user-facing `recommended_action`
//! is derived from their combination rather than dropping them behind a
//! single scalar.
//!
//! Invariants the types enforce:
//! - `LexicalReadinessState` covers the five states any agent must be
//!   able to distinguish: `Missing`, `Repairing`, `StaleButSearchable`,
//!   `Ready`, `CorruptQuarantined`. Ordinary search is correct in
//!   `StaleButSearchable` and `Ready` (and degrading-but-serving in
//!   `Repairing`); it is only unavailable in `Missing` and
//!   `CorruptQuarantined`.
//! - `SemanticReadinessState` covers `Absent`, `Backfilling`,
//!   `FastTierReady`, `HybridReady`, `PolicyDisabled`. Absence and
//!   policy-disabled both mean "no semantic refinement" but have
//!   different operator implications.
//! - `SearchRefinementLevel` describes what a PARTICULAR completed
//!   search actually returned (`LexicalOnly`, `FastTierRefined`,
//!   `FullyHybridRefined`). This is independent of the tier
//!   *readiness* above — a search may be `LexicalOnly` either because
//!   the semantic tier was absent or because the planner chose not to
//!   refine.
//! - `ReadinessSnapshot` groups all three plus a
//!   `RecommendedAction` so every downstream consumer (CLI, TUI,
//!   robot) derives its summary from the same canonical source.

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum LexicalReadinessState {
    /// No usable lexical index exists on disk. Search is unavailable
    /// until a rebuild runs.
    Missing,
    /// A lexical rebuild is actively running; ordinary queries may
    /// return partial results until the rebuild settles.
    Repairing,
    /// The lexical index exists and is byte-consistent but is known to
    /// lag recent DB mutations. Search is fully correct for everything
    /// already indexed; recent ingests may not be visible yet.
    StaleButSearchable,
    /// The lexical index is up to date against the canonical DB.
    Ready,
    /// The lexical index failed validation and has been quarantined
    /// for inspection. Search is unavailable; operator inspection is
    /// required before any auto-recover path is safe.
    CorruptQuarantined,
}

impl LexicalReadinessState {
    /// Whether ordinary search can run against this state. True for
    /// Ready, StaleButSearchable, and Repairing (degraded); false for
    /// Missing and CorruptQuarantined.
    pub(crate) fn is_searchable(self) -> bool {
        matches!(
            self,
            Self::Ready | Self::StaleButSearchable | Self::Repairing
        )
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SemanticReadinessState {
    /// No semantic assets exist. Hybrid refinement is impossible until
    /// an acquisition run lands the required models and embeddings.
    Absent,
    /// Semantic assets are being acquired or backfilled; fast-tier
    /// refinement may become available mid-flight.
    Backfilling,
    /// Fast-tier semantic assets are ready; the quality tier is not
    /// yet available.
    FastTierReady,
    /// Both tiers ready; fully hybrid refinement is possible.
    HybridReady,
    /// The operator explicitly disabled semantic search via policy;
    /// absence is intentional, not a failure condition.
    PolicyDisabled,
}

impl SemanticReadinessState {
    /// Whether the semantic tier can contribute to query refinement at
    /// this state. True only for `FastTierReady` and `HybridReady`.
    pub(crate) fn can_refine(self) -> bool {
        matches!(self, Self::FastTierReady | Self::HybridReady)
    }
}

/// What a completed search actually produced. Independent of tier
/// *readiness* — a search can be `LexicalOnly` either because the
/// semantic tier was absent or because the planner chose not to refine
/// (e.g., a pinned-lexical flag or a fail-open demotion).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SearchRefinementLevel {
    LexicalOnly,
    FastTierRefined,
    FullyHybridRefined,
}

/// Operator / agent-facing remediation recommendation. Derived from a
/// `ReadinessSnapshot` rather than stored; kept as an enum so
/// downstream consumers can pattern-match consistently across CLI,
/// TUI, and robot payloads.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum RecommendedAction {
    /// Everything is converged or acceptably degraded; no user action
    /// needed.
    NothingRequired,
    /// The lexical index is missing or quarantined and must be
    /// rebuilt before search can resume.
    RepairLexicalNow,
    /// A lexical repair is already running. Foreground callers should
    /// attach or wait boundedly instead of starting another rebuild or
    /// reporting the semantic tier as the active wait reason.
    WaitForLexicalRepair,
    /// Lexical search is working; semantic assets are still
    /// converging. Waiting is sufficient.
    WaitForSemanticCatchUp,
    /// Lexical index is stale; a rebuild is recommended to pick up
    /// recent ingests but search continues to work in the meantime.
    RefreshLexicalSoon,
    /// Policy explicitly disabled semantic refinement; nothing to do
    /// beyond acknowledging the degraded search quality.
    SemanticDisabledByPolicy,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct ReadinessSnapshot {
    pub lexical: LexicalReadinessState,
    pub semantic: SemanticReadinessState,
    /// Optional: the refinement level the most recent completed
    /// search actually achieved. `None` when no search has run since
    /// startup.
    #[serde(default)]
    pub last_search_refinement: Option<SearchRefinementLevel>,
}

impl ReadinessSnapshot {
    pub(crate) fn new(lexical: LexicalReadinessState, semantic: SemanticReadinessState) -> Self {
        Self {
            lexical,
            semantic,
            last_search_refinement: None,
        }
    }

    pub(crate) fn with_last_search_refinement(mut self, level: SearchRefinementLevel) -> Self {
        self.last_search_refinement = Some(level);
        self
    }

    /// Derive the recommended operator action from the current
    /// readiness state. Deliberately simple and conservative: the
    /// lexical axis dominates (a broken lexical index is a real
    /// outage; semantic issues are degraded-service at worst).
    pub(crate) fn recommended_action(&self) -> RecommendedAction {
        match self.lexical {
            LexicalReadinessState::Missing | LexicalReadinessState::CorruptQuarantined => {
                RecommendedAction::RepairLexicalNow
            }
            LexicalReadinessState::Repairing => {
                // Lexical repair dominates every semantic state: the
                // foreground contract is attach/wait/fail-open for the
                // active repair, not a second rebuild or a semantic wait.
                RecommendedAction::WaitForLexicalRepair
            }
            LexicalReadinessState::StaleButSearchable => RecommendedAction::RefreshLexicalSoon,
            LexicalReadinessState::Ready => match self.semantic {
                SemanticReadinessState::Absent | SemanticReadinessState::Backfilling => {
                    RecommendedAction::WaitForSemanticCatchUp
                }
                SemanticReadinessState::PolicyDisabled => {
                    RecommendedAction::SemanticDisabledByPolicy
                }
                SemanticReadinessState::FastTierReady | SemanticReadinessState::HybridReady => {
                    RecommendedAction::NothingRequired
                }
            },
        }
    }

    /// Whether ordinary search queries can run at all. Collapses the
    /// two lexical-axis failure modes into a single predicate for
    /// callers that only care about availability.
    pub(crate) fn is_searchable(&self) -> bool {
        self.lexical.is_searchable()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn lexical_states_serialize_as_snake_case() {
        let pairs: &[(LexicalReadinessState, &str)] = &[
            (LexicalReadinessState::Missing, "missing"),
            (LexicalReadinessState::Repairing, "repairing"),
            (
                LexicalReadinessState::StaleButSearchable,
                "stale_but_searchable",
            ),
            (LexicalReadinessState::Ready, "ready"),
            (
                LexicalReadinessState::CorruptQuarantined,
                "corrupt_quarantined",
            ),
        ];
        for (state, expected) in pairs {
            assert_eq!(
                serde_json::to_string(state).unwrap(),
                format!("\"{expected}\"")
            );
        }
    }

    #[test]
    fn semantic_states_serialize_as_snake_case() {
        let pairs: &[(SemanticReadinessState, &str)] = &[
            (SemanticReadinessState::Absent, "absent"),
            (SemanticReadinessState::Backfilling, "backfilling"),
            (SemanticReadinessState::FastTierReady, "fast_tier_ready"),
            (SemanticReadinessState::HybridReady, "hybrid_ready"),
            (SemanticReadinessState::PolicyDisabled, "policy_disabled"),
        ];
        for (state, expected) in pairs {
            assert_eq!(
                serde_json::to_string(state).unwrap(),
                format!("\"{expected}\"")
            );
        }
    }

    #[test]
    fn refinement_levels_serialize_as_snake_case() {
        let pairs: &[(SearchRefinementLevel, &str)] = &[
            (SearchRefinementLevel::LexicalOnly, "lexical_only"),
            (SearchRefinementLevel::FastTierRefined, "fast_tier_refined"),
            (
                SearchRefinementLevel::FullyHybridRefined,
                "fully_hybrid_refined",
            ),
        ];
        for (level, expected) in pairs {
            assert_eq!(
                serde_json::to_string(level).unwrap(),
                format!("\"{expected}\"")
            );
        }
    }

    #[test]
    fn is_searchable_distinguishes_lexical_failure_modes() {
        let cases = [
            (LexicalReadinessState::Missing, false),
            (LexicalReadinessState::CorruptQuarantined, false),
            (LexicalReadinessState::Repairing, true),
            (LexicalReadinessState::StaleButSearchable, true),
            (LexicalReadinessState::Ready, true),
        ];

        for (state, expected) in cases {
            assert_eq!(state.is_searchable(), expected, "{state:?}");
        }
    }

    #[test]
    fn semantic_can_refine_only_when_at_least_fast_tier_ready() {
        let cases = [
            (SemanticReadinessState::Absent, false),
            (SemanticReadinessState::Backfilling, false),
            (SemanticReadinessState::PolicyDisabled, false),
            (SemanticReadinessState::FastTierReady, true),
            (SemanticReadinessState::HybridReady, true),
        ];

        for (state, expected) in cases {
            assert_eq!(state.can_refine(), expected, "{state:?}");
        }
    }

    #[test]
    fn recommended_actions_serialize_as_snake_case() {
        let pairs: &[(RecommendedAction, &str)] = &[
            (RecommendedAction::NothingRequired, "nothing_required"),
            (RecommendedAction::RepairLexicalNow, "repair_lexical_now"),
            (
                RecommendedAction::WaitForLexicalRepair,
                "wait_for_lexical_repair",
            ),
            (
                RecommendedAction::WaitForSemanticCatchUp,
                "wait_for_semantic_catch_up",
            ),
            (
                RecommendedAction::RefreshLexicalSoon,
                "refresh_lexical_soon",
            ),
            (
                RecommendedAction::SemanticDisabledByPolicy,
                "semantic_disabled_by_policy",
            ),
        ];
        for (action, expected) in pairs {
            let expected_json = format!("\"{expected}\"");
            assert!(
                matches!(
                    serde_json::to_string(action).as_deref(),
                    Ok(actual) if actual == expected_json.as_str()
                ),
                "action should serialize as {expected_json}"
            );
        }
    }

    #[test]
    fn recommended_action_missing_lexical_always_repair_now() {
        for sem in [
            SemanticReadinessState::Absent,
            SemanticReadinessState::Backfilling,
            SemanticReadinessState::FastTierReady,
            SemanticReadinessState::HybridReady,
            SemanticReadinessState::PolicyDisabled,
        ] {
            let snap = ReadinessSnapshot::new(LexicalReadinessState::Missing, sem);
            assert_eq!(
                snap.recommended_action(),
                RecommendedAction::RepairLexicalNow
            );
        }
    }

    #[test]
    fn recommended_action_corrupt_lexical_always_repair_now() {
        let snap = ReadinessSnapshot::new(
            LexicalReadinessState::CorruptQuarantined,
            SemanticReadinessState::HybridReady,
        );
        assert_eq!(
            snap.recommended_action(),
            RecommendedAction::RepairLexicalNow
        );
    }

    #[test]
    fn recommended_action_active_lexical_repair_dominates_semantic_state() {
        for sem in [
            SemanticReadinessState::Absent,
            SemanticReadinessState::Backfilling,
            SemanticReadinessState::FastTierReady,
            SemanticReadinessState::HybridReady,
            SemanticReadinessState::PolicyDisabled,
        ] {
            let snap = ReadinessSnapshot::new(LexicalReadinessState::Repairing, sem);
            assert_eq!(
                snap.recommended_action(),
                RecommendedAction::WaitForLexicalRepair
            );
            assert!(snap.is_searchable());
        }
    }

    #[test]
    fn recommended_action_stale_lexical_requests_refresh() {
        for sem in [
            SemanticReadinessState::Absent,
            SemanticReadinessState::HybridReady,
        ] {
            let snap = ReadinessSnapshot::new(LexicalReadinessState::StaleButSearchable, sem);
            assert_eq!(
                snap.recommended_action(),
                RecommendedAction::RefreshLexicalSoon
            );
        }
    }

    #[test]
    fn recommended_action_ready_plus_hybrid_is_nothing_required() {
        let snap = ReadinessSnapshot::new(
            LexicalReadinessState::Ready,
            SemanticReadinessState::HybridReady,
        );
        assert_eq!(
            snap.recommended_action(),
            RecommendedAction::NothingRequired
        );
    }

    #[test]
    fn recommended_action_ready_plus_policy_disabled_acknowledges_policy() {
        let snap = ReadinessSnapshot::new(
            LexicalReadinessState::Ready,
            SemanticReadinessState::PolicyDisabled,
        );
        assert_eq!(
            snap.recommended_action(),
            RecommendedAction::SemanticDisabledByPolicy
        );
    }

    #[test]
    fn recommended_action_ready_plus_semantic_converging_waits() {
        for sem in [
            SemanticReadinessState::Absent,
            SemanticReadinessState::Backfilling,
        ] {
            let snap = ReadinessSnapshot::new(LexicalReadinessState::Ready, sem);
            assert_eq!(
                snap.recommended_action(),
                RecommendedAction::WaitForSemanticCatchUp
            );
        }
    }

    #[test]
    fn snapshot_with_last_search_refinement_round_trips_through_json() {
        let snap = ReadinessSnapshot::new(
            LexicalReadinessState::Ready,
            SemanticReadinessState::FastTierReady,
        )
        .with_last_search_refinement(SearchRefinementLevel::FastTierRefined);

        let json = serde_json::to_string(&snap).unwrap();
        assert!(json.contains("\"lexical\":\"ready\""));
        assert!(json.contains("\"semantic\":\"fast_tier_ready\""));
        assert!(json.contains("\"last_search_refinement\":\"fast_tier_refined\""));

        let parsed: ReadinessSnapshot = serde_json::from_str(&json).unwrap();
        assert_eq!(parsed, snap);
    }

    #[test]
    fn snapshot_defaults_last_search_refinement_to_none() {
        let snap = ReadinessSnapshot::new(
            LexicalReadinessState::Ready,
            SemanticReadinessState::HybridReady,
        );
        assert!(snap.last_search_refinement.is_none());
        let json = serde_json::to_string(&snap).unwrap();
        assert!(json.contains("\"last_search_refinement\":null"));
    }
}