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
//! Node-deletion's above-storage half.
//!
//! `GraphWrite::remove_node` takes the node out of the backend. Four pieces
//! of state that a deleted node owns live one layer *above* the backend, on
//! `DirGraph` itself, so nothing inside storage can see them go — and each is
//! readable only while the node still exists. They are removed here, in the
//! same loop as the backend removal and in the order that keeps each read
//! valid.
//!
//! Extracted from `maintain.rs` to keep that file under the god-file LoC
//! ceiling. The index/bucket sweeps that follow a deletion stay there, in
//! `detach_delete_nodes`, which is this module's only caller.
use HashSet;
use NodeIndex;
use crateDirGraph;
use crateGraphWrite;
/// Drop `node_idx`'s vector from every embedding store that holds one, and
/// journal each removal so a statement rollback can put it back.
///
/// **Why deletion must reach this map.** `EmbeddingStore` is keyed by the
/// global `NodeIndex`, and `StableDiGraph` hands a freed index straight to
/// the next node created. A vector left behind is therefore not merely stale
/// bookkeeping: the next node to land on that slot — of *any* type, embedded
/// or not — inherits it and comes back as a full-similarity top hit from
/// `vector_search`, on both the scan and the HNSW path.
///
/// Costs one hash probe per store, and stores are per `(node_type, property)`
/// — a handful, independent of graph size. The `is_empty` guard keeps the
/// overwhelmingly common un-embedded graph at zero cost per deleted node.
/// Drop `node_idx`'s document from every text index that holds one.
///
/// **Why deletion must reach this map**, and why it is not journalled the way
/// [`prune_doomed_embeddings`] is. A text index addresses documents *by*
/// `NodeIndex`, and `StableDiGraph` hands a freed index straight to the next
/// node created, so a document left behind is inherited: the new node — of any
/// type, of any content — scores as the deleted one's text. That is a wrong
/// answer, so the prune is unconditional.
///
/// A rolled-back delete would therefore leave the node restored and its
/// document gone, so the prune journals `UndoEntry::TextDocPruned` — which
/// carries no pre-image and instead marks the slot for the next refresh to
/// re-read. Deriving the document again from the text the rollback restores is
/// both cheaper than keeping a second copy of every deleted document and
/// exactly what a rebuild would produce.
///
/// The journal only survives a *reversal*: a committed delete discards it, so
/// deleting a million nodes prunes a million documents and leaves the index
/// with an empty dirty set. Deletion is not staleness.
///
/// Costs one hash probe per index, and indexes are per `(node_type, property)`
/// — a handful, independent of graph size. The `is_empty` guard keeps the
/// overwhelmingly common un-indexed graph at zero cost per deleted node.
/// Remove each doomed node from storage, carrying the four pieces of state
/// that live *above* storage and so cannot be recovered afterwards.
///
/// All four are read while the node still exists and are lost the moment it
/// does not, which is why they are here rather than in `detach_delete_nodes`'
/// sweeps:
///
/// - **The change-capture before-image's labels.** The capture wrapper reads
/// the node's properties and title as it removes it, but secondary labels
/// live in `DirGraph::secondary_label_index`, one layer above the backend.
/// A delete is the one event whose only informative half is `before`, so an
/// image missing its labels is the whole loss.
/// - **The dropped timeseries entry.** `timeseries_store` is O(V) and so is
/// deliberately not part of the checkpoint's schema clone; statement
/// rollback recovers it from the undo journal instead.
/// - **The node's embeddings.** Same ownership story as the timeseries, with a
/// sharper failure mode: the freed `NodeIndex` is reused, so a vector left
/// behind is inherited rather than merely orphaned. See
/// [`prune_doomed_embeddings`].
/// - **The node's text-index documents.** The same inheritance hazard, one
/// layer further up: a BM25 document is addressed by `NodeIndex` directly.
/// See [`prune_doomed_text_docs`].
pub