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
//! Indexing each audited repository in trusty-search before the report is
//! rendered (#5670, DOC-67 §6 "Still open").
//!
//! Why: `tga audit` always renders with `trusty-review report --analyze`, and
//! that renderer fetches nothing for a repository trusty-search does not serve.
//! Its membership check — `HttpAnalyzeMetricsSource::index_served`, a `GET
//! /indexes` — misses for a repository nobody indexed, and the miss is
//! fail-open: `AnalyzeGap::NotIndexed`, one gap line, exit 0, and the findings
//! table, the complexity distribution and the health factors all render empty.
//! Nothing in the audit path indexed anything, so an unindexed repository
//! produced a hollow report over a clean exit. Starting the analyze daemon
//! ([`super::analyze`]) closed link 3 of the prerequisite chain; this module
//! closes link 2, the per-repository index.
//!
//! What: [`ensure_repositories_indexed`], the `trusty-search` binary resolution
//! rule it applies, and the per-repository [`RepoIndexOutcome`] it returns.
//! Indexing runs as a subprocess — the sanctioned route, since DOC-67 §5
//! forbids a second HTTP-client implementation against the daemon, and the same
//! house pattern [`super::review`] uses to reach `trusty-review`.
//!
//! ## This one is fail-open, and the analyze preflight is not
//!
//! [`super::analyze`] refuses the whole run when the daemon will not start,
//! because that failure takes every repository's analysis with it. A repository
//! that will not index takes only its own, and DOC-67 §9 fixes what happens to a
//! per-repository failure: the repository is excluded, named in Gaps & Caveats,
//! and the run continues. A one-shot sweep over an org cannot spend its one shot
//! on the first repository with a broken checkout. So every failure here becomes
//! a gap line ([`super::index_gap_lines`]); none of them aborts the sweep, and
//! none of them changes the exit status.
//!
//! ## The index id is a cross-process contract
//!
//! trusty-review derives the id it looks up from the checkout path written into
//! `manifest.toml`, which is the only thing the renderer reads. [`index_id_for`]
//! derives it from the same value through the same function
//! ([`trusty_common::derive_checkout_index_id`], #6149) — an id derived any
//! other way indexes every repository under a name nobody ever queries, leaving
//! the reports exactly as hollow as before while looking fixed.
//!
//! ## What the wait does, and does not, guarantee
//!
//! `trusty-search index` is invoked with its default foreground wait, which
//! detaches after the index stops making progress rather than blocking forever.
//! That bounds an unattended run (DOC-67 §2) at the cost of the strongest
//! claim: a detached index is registered and answers the renderer's membership
//! check while still filling. Its sections then render from whatever had been
//! embedded — thinner than a completed pass, and not distinguishable from one
//! here. Waiting forever instead would trade that for a sweep that can hang.
//! Test: `super::tests`; `super::real_binary_tests` for the CLI facts this rests
//! on.
//!
//! # Spec References
//! - [`SPEC-TGAUDIT-06~draft`](../../../../docs/specs/DOC-67-tga-audit-mode.md#SPEC-TGAUDIT-06~draft)
//! - [`SPEC-TGAUDIT-09~draft`](../../../../docs/specs/DOC-67-tga-audit-mode.md#SPEC-TGAUDIT-09~draft)
use OsString;
use Path;
use Command;
use crateDdRepositoryEntry;
/// Environment variable that overrides the `trusty-search` binary path.
///
/// Why: the same override-then-PATH resolution [`super::review::ENV_REVIEW_BIN`]
/// and [`super::analyze::ENV_ANALYZE_BIN`] give the other two sibling binaries,
/// so an engagement that pins its tools (`trusty-audit` exports pinned paths
/// onto every `tga audit` child) can pin this one the same way.
pub const ENV_SEARCH_BIN: &str = "TRUSTY_SEARCH_BIN";
/// Default binary name searched on PATH.
pub const DEFAULT_SEARCH_BIN: &str = "trusty-search";
/// What became of one repository's index.
///
/// Why: the caller needs the distinction between "this repository is now
/// analyzable" and "this repository's report sections are not assessed" — the
/// second is a Gaps & Caveats line, the first is silence.
/// What: served before the audit started, indexed by this run, or failed with
/// the reason.
/// Test: `super::tests::an_unindexed_repository_is_indexed_before_the_render`.
/// One repository's indexing result.
///
/// Why: the gap lines name repositories the way the report does, so the outcome
/// carries the manifest's display name rather than a path the reader has never
/// seen.
/// What: the display name, the index id the renderer will look up (`None` when
/// none could be derived), and the status.
/// Test: `super::tests::one_repository_that_fails_to_index_does_not_stop_the_others`.
/// The `trusty-search` binary this process will invoke.
///
/// Why/What: [`ENV_SEARCH_BIN`] when set to a non-empty value, else
/// [`DEFAULT_SEARCH_BIN`] resolved on PATH by the OS. Reading the variable is all
/// this does; the rule lives in [`binary_from_override`] so tests never call
/// `std::env::set_var`, which is `unsafe` in edition 2024 and unsound under the
/// parallel harness (#5308 review).
/// Test: `super::tests::search_binary_resolution_prefers_the_env_override`.
/// The resolution rule itself: an override wins unless it is empty.
///
/// Why/What/Test: see [`resolve_search_binary`].
pub
/// The trusty-search index id for a checkout path.
///
/// Why: this must agree with `trusty_review::report::analyze_adapter::
/// derive_index_id`, which is what the renderer looks the index up by. Until
/// #6149 both were the checkout BASENAME, copied rather than called — and two
/// checkouts of one repository therefore collided on one id, so the sweep
/// indexed one tree and the renderer read another. Both now call
/// [`trusty_common::derive_checkout_index_id`], so the agreement is a call.
/// What: `"<slugified basename>-<8 hex over the canonical path>"` — `None` for a
/// path with no final component, e.g. `/`.
/// Test: `super::tests::{the_index_id_distinguishes_two_checkouts_of_one_repo,
/// index_ids_match_the_manifest_paths_the_renderer_reads}`.
/// The membership probe's argument vector.
///
/// Why: `index-status <id>` asks the daemon for `/indexes/<id>/status` and exits
/// non-zero on its 404, which is the same question the renderer's `index_served`
/// asks — so a repository that would satisfy the renderer is not re-indexed.
/// Building the vector in a pure function is what lets a test assert it without
/// spawning anything, as [`super::review::report_args`] does.
/// Test: `super::tests::the_index_invocation_names_the_path_and_the_id` and
/// `super::tests::an_already_indexed_repository_is_not_reindexed` — the first
/// asserts the vector without spawning, the second that a served repository is
/// probed with `index-status <id>` and nothing else is spawned.
pub
/// The indexing invocation's argument vector.
///
/// Why: `--name` is what binds the index to the id the renderer will look up;
/// without it trusty-search would name the index after whatever directory the
/// audit happens to be running from.
/// What: `index <path> --name <index_id>`.
/// Test: `super::tests::the_index_invocation_names_the_path_and_the_id`.
pub
/// Index every audited repository that trusty-search does not already serve.
///
/// Why/What: see the module docs. Resolves the binary from the environment once,
/// at this entry point, and delegates to [`ensure_repositories_indexed_with`].
/// Takes the manifest's own repository entries rather than the config so the
/// index ids are derived from the exact paths the renderer will read.
///
/// Never returns an error: a failure is a per-repository gap, not a run
/// failure (DOC-67 §9).
///
/// Test: `super::tests::an_unindexed_repository_is_indexed_before_the_render`.
pub async
/// [`ensure_repositories_indexed`] with the binary already resolved.
///
/// Why: taking it as a parameter is what lets a test drive the whole
/// probe-and-index path against a stub executable without touching the process
/// environment (#5308 review).
/// What: runs the repositories one at a time on the blocking pool — an indexing
/// pass is CPU- and I/O-heavy at the daemon, and running an org's worth of them
/// at once would make each slower rather than the set faster. Order is the
/// manifest's, so two runs over the same state produce the same gap lines.
/// Test: `super::tests::{an_already_indexed_repository_is_not_reindexed,
/// one_repository_that_fails_to_index_does_not_stop_the_others}`.
pub async
/// Probe one repository, index it if the daemon does not already serve it.
///
/// Why: the probe is what keeps an audit over an already-indexed org cheap — a
/// re-index of an unchanged repository is wasted embedding time the one-shot run
/// does not have to spend.
/// What: derives the id, probes, and on a miss spawns the indexing pass. Every
/// outcome is a value; nothing here panics or propagates.
/// Test: `super::tests::an_already_indexed_repository_is_not_reindexed`.
/// Echo a failed outcome to stderr, then hand it back unchanged.
///
/// Why: the gap line reaches the operator only once the report exists, and the
/// audit can run for many minutes after this point. The run log is where someone
/// watching finds out — the same reason [`super::analyze`] narrates its spawn.
/// Whether trusty-search already serves `index_id`.
///
/// Why: exit status is the whole answer, so nothing here parses another crate's
/// JSON. A probe that cannot run at all (no binary, no daemon) answers "no",
/// which sends the repository down the indexing path — where the same fault
/// produces a named gap instead of a silent skip.
/// Test: `super::tests::an_already_indexed_repository_is_not_reindexed`.
/// Run `binary` with `args`, capturing both streams.
///
/// `Command::output` gives the child a null stdin, which is what keeps DOC-67
/// §2's no-prompt rule true of the children as well as of the sweep.
/// The operator-facing text for a child that would not start.
///
/// Why: "not installed" is a one-line fix and every other spawn failure is not,
/// so the two do not share a message. The remedy names both ways to supply the
/// binary, since an engagement that pins its tools uses the override rather than
/// PATH.
/// Test: `super::tests::a_missing_search_binary_is_named_and_the_run_continues`.
/// The last non-blank line of a child's output.
///
/// Why: a failing `trusty-search index` prints its cause last, and the lines
/// above it are progress the report's reader has no use for.