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
//! Non-destructive reindex staging decisions (#603).
//!
//! Why: before this change, only a `--force` reindex staged its rebuilt corpus
//! in `index.redb.tmp` and atomically swapped it in on success. The standard
//! (non-force) reindex wrote chunks straight into the live `index.redb`, so a
//! reindex that failed mid-way — or, post-#601, embedded zero vectors — destroyed
//! the only searchable copy with no way to roll back. During the Duetto P0 this
//! turned a transient embedder outage into a permanently dead index.
//!
//! What: the actual redb staging/swap plumbing (open-fresh tmp, swap onto the
//! indexer, rename-on-commit, discard-on-abort) lives in `super` because it
//! needs private indexer internals. This module owns the *pure decision* that
//! drives that plumbing: given whether a durable corpus exists and the terminal
//! [`super::validate::ReindexOutcome`], decide whether to (a) stage at all and
//! (b) commit the swap or roll it back. Keeping the decision pure makes the
//! safety-critical "never promote a failed/empty corpus" rule unit-testable
//! without a live daemon.
//!
//! Test: `super::staging::tests` covers every branch of both helpers.
use ReindexOutcome;
/// Whether a reindex should stage into `index.redb.tmp` (non-destructive) or
/// write directly into the live `index.redb`.
///
/// Why: staging is now the default safety net for *every* reindex with a
/// durable corpus, not just `--force`. Indexes without a durable corpus
/// (BM25-only, ephemeral test indexers) cannot stage — there is no file to
/// swap — so they fall back to the legacy direct-write path.
/// What: returns `true` (stage) iff the index has a durable corpus store.
/// `force` no longer gates staging: a clean non-force reindex is just as
/// entitled to a rollback-safe rebuild as a forced one.
/// Test: `should_stage_*` below.
pub
/// Resolution of a finished, staged reindex: promote the staging corpus or
/// discard it and keep the live one.
///
/// Why: the orchestrator must make exactly one of two mutually-exclusive moves
/// on a staged reindex — atomically rename the tmp over the live file
/// (`Commit`) or delete the tmp and re-open the untouched live file
/// (`Rollback`). Modelling it as an enum guarantees the call site handles both
/// and never silently does neither.
/// What: `Commit` promotes; `Rollback { reason }` discards and surfaces why.
/// Test: `resolve_staging_*` below.
pub
/// Decide whether a staged reindex commits its swap or rolls back (#603 + #601).
///
/// Why: this is the single chokepoint that ties the non-empty validation gate
/// (#601) and memory-abort handling to the atomic swap (#603). A reindex only
/// promotes its freshly-staged corpus when it is (1) not memory-aborted and
/// (2) classified [`ReindexOutcome::Ready`]. Any other terminal state rolls
/// back, leaving the previous live corpus intact — exactly the safety net the
/// P0 needed.
/// What: returns `Rollback` when `memory_aborted` is true (partial corpus must
/// not be promoted) or when `outcome` is `Failed` (zero-vector embed failure);
/// otherwise `Commit`. The reason string is forwarded so the caller can log /
/// surface it.
/// Test: `resolve_staging_*` below — covers ready→commit, failed→rollback,
/// memory-abort→rollback, and the precedence (memory-abort wins over a Ready
/// outcome).
pub