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
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
//! Invoking `trusty-review report` as a subprocess (#5238, DOC-67 §6 step 3).
//!
//! Why: tga and trusty-review meet at a file, not at a Cargo edge (DOC-67 §5) —
//! which leaves someone to actually run the renderer. trusty-review already
//! treats a sibling trusty-* binary as an invocable subprocess
//! (`SubprocessAnalyzeClient`, closing #632); AUDIT follows that same house
//! pattern in the other direction rather than inventing a second idiom or
//! taking a dependency edge that would risk an import cycle.
//! What: [`resolve_review_binary`], [`run_review_report`], and the
//! [`ReviewRun`] record of one invocation. The binary is `TRUSTY_REVIEW_BIN`
//! when set, else `trusty-review` on PATH — the same override-then-PATH
//! resolution `SubprocessAnalyzeClient` uses for `trusty-analyze`.
//! Test: `super::tests`.
use ;
use Command;
/// Environment variable that overrides the `trusty-review` binary path.
///
/// Why: lets an operator or a test pin the exact binary without touching PATH,
/// matching `TRUSTY_ANALYZE_BIN`'s role on the trusty-review side.
pub const ENV_REVIEW_BIN: &str = "TRUSTY_REVIEW_BIN";
/// Default binary name searched on PATH.
pub const DEFAULT_REVIEW_BIN: &str = "trusty-review";
/// Failures invoking the renderer.
///
/// Why: a library module, so a typed error. The distinction that matters to an
/// operator is "you have not installed the renderer" versus "the renderer ran
/// and failed" — the first is a one-line fix, the second needs the child's own
/// output, which the caller has already printed.
/// What: binary-not-found (carrying the remediation), any other spawn failure,
/// and a join failure from the blocking pool.
/// Test: `super::tests::missing_binary_is_a_named_actionable_error`.
/// The result of one `trusty-review report` invocation.
///
/// Why: DOC-67 §6 step 4 requires the child's exit code and both streams to
/// reach the operator, and step 5 requires the artifact paths — so the whole
/// invocation is one value the caller reports on, rather than side effects the
/// caller has to reconstruct.
/// What: the artifact paths trusty-review printed to stdout, both captured
/// streams, and the exit status. A non-zero exit is recorded here, not raised —
/// the caller decides what a failed render means for the run.
/// Test: `super::tests::artifact_paths_are_parsed_from_stdout`.
/// The `trusty-review` binary this process will invoke.
///
/// Why/What: `TRUSTY_REVIEW_BIN` when set to a non-empty value, else
/// [`DEFAULT_REVIEW_BIN`] resolved on PATH by the OS. Reading the variable is
/// all this function does; the rule itself lives in [`binary_from_override`] so
/// it can be tested without mutating the process environment.
/// Test: `super::tests::binary_resolution_prefers_the_env_override`.
/// The resolution rule itself: an override wins unless it is empty.
///
/// Why: taking the override as a parameter keeps the rule a pure function, so
/// the tests never call `std::env::set_var`. That call is `unsafe` in edition
/// 2024 because another thread reading the environment concurrently is UB, and
/// `cargo test` runs tests in parallel — a test-only guarantee of
/// single-threadedness does not exist (#5308 review).
/// What: `None` and `Some("")` both fall back to [`DEFAULT_REVIEW_BIN`].
/// Test: `super::tests::binary_resolution_prefers_the_env_override`.
pub
/// Render `manifest` into `out_dir` by invoking `trusty-review report`.
///
/// Why: the last step of DOC-67 §6's orchestration, and the only one that
/// produces the deliverable. It runs on the blocking pool because the child can
/// take minutes on a large repository set and must not occupy an async worker.
/// What: spawns `<binary> report --manifest <manifest> --analyze --synthesize
/// --out <out_dir>`, captures both streams and the exit status, and parses the
/// artifact paths from stdout. A non-zero exit returns `Ok` with
/// [`ReviewRun::success`] false — only a failure to *start* the child is an
/// `Err`, because that is the one case where the caller has nothing to report.
///
/// The `--analyze` flag is always passed: an AUDIT report without the live
/// analysis pass has no findings, no complexity distribution, and no health
/// factors (DOC-67 §8), and its absence is reported as a gap by trusty-review
/// rather than being silently accepted here.
///
/// `--synthesize` is always passed too (#5454). trusty-review 0.15 makes
/// synthesis unconditional and treats the flag as a deprecated no-op, but tga
/// resolves that binary from PATH rather than from a Cargo edge (DOC-67 §5), so
/// the installed copy may predate this change — on an older one the flag is the
/// only thing that turns inference on at all.
/// [`require_review_supports_required_inference`] normally rejects such a copy
/// before the sweep starts; the flag still matters for the copy whose version it
/// could not read.
/// Test: `super::tests::{missing_binary_is_a_named_actionable_error,
/// artifact_paths_are_parsed_from_stdout, invocation_requests_inference}`.
///
/// # Errors
///
/// [`ReviewRunError::BinaryNotFound`] when the binary is not installed,
/// [`ReviewRunError::Spawn`] for any other start failure, and
/// [`ReviewRunError::Join`] if the blocking task is cancelled.
pub async
/// [`run_review_report`] with the binary already resolved.
///
/// Why: the environment is read exactly once, at the public entry point, so a
/// test can drive the whole spawn-and-map path at a binary that certainly does
/// not exist without touching the process environment (#5308 review).
/// What: everything `run_review_report` does apart from resolution.
/// Test: `super::tests::missing_binary_is_a_named_actionable_error`.
pub async
/// The synchronous half of [`run_review_report`].
///
/// Why: isolated so it carries no async context into the blocking pool, the
/// same split `SubprocessAnalyzeClient` uses.
/// What: builds the command, runs it to completion, maps `NotFound` onto the
/// actionable error, and parses stdout.
/// Test: exercised through [`run_review_report`].
/// The exact argument vector [`invoke`] hands `trusty-review`.
///
/// Why: this list IS the tga→trusty-review contract, and the website documents it
/// verbatim as the by-hand recovery command. Building it in a pure function is
/// what lets a test assert its contents without spawning anything — the same
/// reason [`binary_from_override`] takes its input as a parameter.
/// What: `report --manifest <m> --analyze --synthesize --out <dir>`.
/// Test: `super::tests::invocation_requests_inference`.
pub
/// Environment variable carrying the credential the renderer's inference needs.
///
/// Why: named through `trusty_common` so tga and trusty-review cannot drift on
/// the spelling — trusty-review reads the same constant to build its provider.
pub const ENV_INFERENCE_CREDENTIAL: &str = ENV_OPENROUTER_API_KEY;
/// The audit cannot start without an inference credential.
///
/// Why: #5454 made the DD report's narrative required, and DOC-67 §2 gives the
/// sweep exactly one non-interactive shot. Those two together decide WHERE this
/// check goes: a sweep can run for many minutes over an org, and discovering an
/// unset key at the render step — the last thing it does — throws all of it away
/// for a fault that was knowable before stage 1.
/// What: a typed error naming the variable and how to set it. Only OpenRouter is
/// checked, per the #5454 owner decision that it is the audit's only inference
/// path. The value is tested for emptiness and never read into a message.
/// Test: `super::tests::{absent_credential_is_a_named_actionable_error,
/// present_credential_passes_the_precheck}`.
;
/// Check that the inference credential is present, before the sweep starts.
///
/// Why/What: reads [`ENV_INFERENCE_CREDENTIAL`] and applies
/// [`credential_is_present`]. Reading the variable is all this does; the rule
/// lives in that pure function so tests never call `std::env::set_var`, which is
/// `unsafe` in edition 2024 and unsound under parallel tests (#5308 review).
///
/// # Errors
///
/// [`MissingInferenceCredential`] when the variable is unset or blank.
///
/// Test: `super::tests::absent_credential_is_a_named_actionable_error`.
/// The presence rule itself: set, and not only whitespace.
///
/// Why: a variable exported as an empty string is the shape a half-finished
/// shell profile leaves behind, and it fails at the provider exactly as an unset
/// one does — so the preflight must treat them the same.
/// What: `None`, `Some("")` and `Some(" ")` are all absent.
/// Test: `super::tests::present_credential_passes_the_precheck`.
pub
/// The oldest `trusty-review` whose report is guaranteed to carry inference.
///
/// Why: 0.15.0 is the release that made synthesis unconditional and turned every
/// degrade path into a hard error (#5454). A 0.14 renderer accepts
/// `--synthesize`, falls back to a deterministic, narrative-free report on any
/// provider failure, and still exits 0.
pub const MIN_REVIEW_VERSION: = ;
/// The installed renderer predates required inference.
///
/// Why: tga resolves `trusty-review` from PATH, not from a Cargo edge (DOC-67
/// §5), so the two versions are installed and upgraded separately and this
/// pairing is ordinary rather than exotic. The remedy is one command, and the
/// message has to say which command — "your report has no narrative" is a
/// symptom the operator cannot act on.
/// What: the binary that was asked, the version it reported, and the floor.
/// Test: `super::tests::stale_renderer_is_rejected_before_the_sweep`.
/// Reject a pre-0.15 renderer before the sweep starts.
///
/// Why: the version skew is the second whole-run precondition that is knowable
/// up front, alongside the credential — and DOC-67 §2 gives this command one
/// non-interactive shot, so an operator who learns about it only after eight
/// stages have run learns it at the worst possible moment. The check itself is
/// one process spawn that returns in milliseconds.
/// What: runs `<binary> --version` and compares against [`MIN_REVIEW_VERSION`].
/// A binary that cannot be spawned, exits non-zero, or prints something this
/// cannot parse is ALLOWED THROUGH deliberately: the not-installed case already
/// has a better-worded error at the render step, and a renderer whose version
/// cannot be read is still caught by
/// [`require_rendered_report_carries_synthesis`], which checks the delivered
/// artifact rather than a claim about it. This narrows the window early; it is
/// not the thing that closes it.
///
/// # Errors
///
/// [`ReviewBinaryTooOld`] when the binary reports a version below the floor.
///
/// Test: `super::tests::stale_renderer_is_rejected_before_the_sweep`.
/// The version rule itself: reject only what is definitely below the floor.
///
/// Why: taking the reported text as a parameter keeps the decision — not merely
/// the parse — testable without a binary to spawn, the same split
/// [`binary_from_override`] and [`credential_is_present`] use for their rules.
/// What: `Err` when the parsed version is below [`MIN_REVIEW_VERSION`]; `Ok` for
/// anything at or above it, and for output this cannot read at all.
/// Test: `super::tests::stale_renderer_is_rejected_before_the_sweep`.
pub
/// Read `major.minor.patch` out of a `--version` line.
///
/// Why: a pure function so the comparison is tested without a binary to spawn,
/// the same split [`binary_from_override`] and [`credential_is_present`] use.
/// What: takes the last whitespace-separated token of the first non-empty line
/// (`trusty-review 0.14.1` → `0.14.1`), drops a leading `v`, and reads the three
/// leading numeric components; a pre-release or build suffix is cut at the first
/// `-` or `+`. `None` for anything it cannot read, which the caller treats as
/// "proceed" rather than "too old".
/// Test: `super::tests::stale_renderer_is_rejected_before_the_sweep`.
pub
/// The delivered report could not be shown to carry a written analysis.
///
/// Why: the child's exit status is not evidence of a synthesis pass, so the
/// artifact itself is what gets checked — and a check that cannot be performed
/// must fail rather than pass, or it re-opens the hole it was added to close.
/// What: `NoSynthesis` when the report JSON was read and carries no verified
/// narrative — the pre-0.15 degrade, whose remedy is an upgrade; `NotCheckable`
/// when the JSON was missing, unreadable, or not JSON, whose cause is local.
/// Test: `super::tests::{exit_zero_over_a_narrative_free_report_is_a_failure,
/// an_uncheckable_report_fails_rather_than_passes}`.
/// Require that the report trusty-review just wrote carries a written analysis.
///
/// Why: `ReviewRun::success` is `output.status.success()` and nothing else, and
/// a pre-0.15 renderer exits 0 over a report whose narrative sections were never
/// written by a model. Checking the exit status alone is what let #5454's defect
/// survive in the mid-run-provider-failure arm — an audit that reports a clean
/// pass while delivering the very report the ticket exists to abolish.
/// What: finds the `.json` twin among the artifact paths the child printed and
/// requires its `synthesis` object to carry at least one verified field. That
/// is the invariant 0.15 guarantees (a pass with nothing left is
/// `SynthesisError::NoVerifiableContent`, which fails the render), so it holds
/// regardless of what version produced the file — including a future one whose
/// degrade shape nobody here anticipated.
///
/// # Errors
///
/// [`UnverifiedReport`] — see its variants.
///
/// Test: `super::tests::{exit_zero_over_a_narrative_free_report_is_a_failure,
/// a_synthesized_report_passes_the_check,
/// an_uncheckable_report_fails_rather_than_passes}`.
/// The synthesis rule itself, over the report JSON's text.
///
/// Why: a pure function over a string, so the pre-0.15 degraded shape and the
/// 0.15 shape are both tested as literal fixtures rather than reconstructed from
/// whichever version of `Synthesis` happens to be linked in.
/// What: `Some(true)` when `synthesis` holds a non-blank `executive_summary`, a
/// non-empty `top_risks`, or a non-empty `findings`; `Some(false)` for valid
/// JSON without one — which covers 0.14's `"status": {"state": "unavailable"}`
/// shape, whose prose fields are all empty, and an absent `synthesis` key.
/// `None` when the text is not JSON at all.
/// Test: `super::tests::exit_zero_over_a_narrative_free_report_is_a_failure`.
pub
/// Parse the written-artifact paths from trusty-review's stdout.
///
/// Why: trusty-review prints its progress to stderr and the written paths to
/// stdout precisely so a caller can consume them; DOC-67 §6 step 5 is that
/// caller.
/// What: every non-blank stdout line, trimmed, in order.
/// Test: `super::tests::artifact_paths_are_parsed_from_stdout`.