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
//! The owner-authored keep-list: worktrees no sweep may ever propose (#6927).
//!
//! Why: every gate in [`classify`](super::worktree_reclaim::classify) answers a
//! question ABOUT the worktree — who claims it, whether its branch landed,
//! whether it holds unsaved work. None of them can express the operator's own
//! standing decision that a particular tree is off limits whatever those
//! answers turn out to be. DOC-73 §16.2 asks for exactly that as a first-class
//! gate, so a keep-listed worktree renders `Blocked` with a reason rather than
//! being filtered out of the survey and becoming invisible.
//! What: [`KeepList`], built from the operator's `disk.keep_list` patterns
//! (`~/.trusty-tools/trusty-mpm/config.yaml`). An entry is either a PATH — which
//! protects that directory and everything under it — or a glob. A pattern that
//! will not compile is not silently dropped: it is recorded in
//! [`KeepList::invalid`] so the surfaces that render the list can say the
//! operator's intent did not take effect.
//!
//! # A list that could not be READ keeps everything
//!
//! A protective list has one intolerable failure mode: reading it wrong and
//! concluding nothing is protected. `TrustyToolsConfig::load` collapses any
//! YAML error to defaults so a bad config can never abort startup, and under
//! that rule a typo anywhere in the file — in a section nothing here reads —
//! left `disk` at `None`, the keep-list empty, and the sweep free to delete a
//! worktree the operator had vetoed. So the list carries a third state:
//! [`KeepList::unreadable`], which
//! [`load_disk_keep_list`](crate::core::trusty_tools_config::load_disk_keep_list)
//! builds when the config will not parse. Every path matches it, which turns
//! gate 0 into a refusal of the whole pass and names the config error in the
//! reason. Startup is still never aborted; only DELETION is.
//! Test: `worktree_keep_list_tests`.
use ;
use ;
/// The characters that make an entry a GLOB rather than a path.
///
/// Why: an operator writing `/Users/me/hotstats` means a directory, and a
/// directory protects its contents; one writing `**/hotstats*` means a match
/// rule. Deciding by spelling rather than by a second config key keeps one
/// list able to express both without the operator declaring which is which.
const GLOB_CHARS: = ;
/// One compiled keep-list entry.
/// Why a worktree is kept: an operator entry, or an unreadable keep-list.
///
/// Why: the two are not interchangeable to the operator reading the refusal.
/// One says "you asked for this"; the other says "your config is broken and
/// nothing will be reclaimed until you fix it". Carrying the distinction as
/// DATA lets every surface word it identically and keeps none of them matching
/// on a message string.
/// Test: `an_unreadable_keep_list_keeps_every_path`,
/// `a_literal_path_entry_keeps_the_directory_and_its_children`.
pub
/// The operator's standing "never touch these worktrees" list (#6927).
///
/// Why: see the module docs. This is a protective list, so the only failure
/// mode that matters is an entry that silently protects nothing — an operator
/// who wrote a bad glob believes their tree is safe. Hence [`invalid`], which
/// every surface that reports the keep-list also reports.
/// What: compiled entries plus the patterns that would not compile. An empty
/// list matches nothing, which is the default and is a no-op gate; a list built
/// by [`Self::unreadable`] matches EVERYTHING — see the module docs.
/// Test: `worktree_keep_list_tests`.
pub
/// Expand a leading `~` against `$HOME`, leaving every other spelling alone.
///
/// Why: the config file is hand-written, and `~/hotstats` is how an operator
/// spells a home-relative directory. `workspace_root_template` already accepts
/// that spelling, so the keep-list accepting it too is consistency rather than
/// a new convention.
/// Test: `a_tilde_path_expands_against_home`.
/// Canonicalize, falling back to the path as given.
///
/// Why: both sides of every comparison have to be in the same form or a
/// symlinked workspace root compares as unrelated to the worktree inside it.
/// Canonicalization fails for a directory that no longer exists — a stale
/// worktree pointer, which is precisely a case this gate must still answer for
/// — so a failure falls back rather than refusing to compare.
///
/// #7504: shared with
/// [`worktree_reclaim_launch`](super::worktree_reclaim_launch), whose gate
/// compares the same two forms of the same candidate paths. One resolver, so the
/// two gates cannot disagree about whether a symlinked workspace root contains a
/// worktree.
pub