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
//! What:
//! The standards-review shadow run, end to end and in-process: classify the landed commit's names, read the commit,
//! build the capped input, call the gateway, and append the one log entry. Also a bounded runner that gives a caller a
//! hard time limit over all of it.
//! Why:
//! This replaces the root `mise.toml` task `shadow-review` (its Luna route; the `SHADOW_REVIEW_ALLOW=1` route starts
//! `pi` and is not ported). The review must never delay or fail a landing, so nothing here panics or returns a failure
//! the landing would act on, and `run_within` abandons the work when the time is up. Nothing calls this yet.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const outcome = await reviewLanded({ names, sha, subject, read, options, clock });
//! ```
use super::iso_time::format_iso_milliseconds;
use super::standards_review_client::{Gateway, Reply, SCRIPT_DEADLINE, post_decision};
use super::standards_review_append::{deliver, names_commit};
use super::standards_review_entry::{ReviewEntry, render_review, render_skipped};
use super::standards_review_input::{LOG_RELATIVE, LandedCommit, Plan, ReviewInput, build_input, plan_for};
use super::standards_review_request::{ENDPOINT, Judged, read_answer, request_body};
use super::standards_review_text::seconds_with_tenth;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
/// The environment variable that points the log at a disposable fixture.
pub const LOG_VARIABLE: &str = "SHADOW_REVIEW_LOG";
/// The variable that replays a commit other than `HEAD`.
pub const SHA_VARIABLE: &str = "SHADOW_REVIEW_SHA";
/// The variable that asks for the full review route, which this port does not run.
pub const ALLOW_VARIABLE: &str = "SHADOW_REVIEW_ALLOW";
/// The credential variable, and the stop-hook credential used when it is unset.
pub const KEY_VARIABLES: [&str; 2] = super::service_review_key::KEY_VARIABLES;
/// The failure line written when no credential is set.
pub const NO_KEY_MESSAGE: &str = "no SHADOW_REVIEW_LLMGATEWAY_API_KEY or STOP_REMINDER_LLMGATEWAY_API_KEY in the environment";
/// What:
/// What the run was told: the log, the credential, and where and how long to call.
/// Why:
/// Production passes `ENDPOINT` and `SCRIPT_DEADLINE`; tests pass a loopback URL and a short deadline.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReviewOptions = { logPath: string; apiKey?: string; endpoint: string; deadline: number };
/// ```
#[derive(Clone, Debug)]
pub struct ReviewOptions {
/// Where entries are appended.
pub log_path: PathBuf,
/// The bearer credential; `None` writes the no-credential failure entry.
pub api_key: Option<String>,
/// The decisions endpoint.
pub endpoint: String,
/// The gateway call's total time.
pub deadline: Duration,
/// A PEM authority replacing the embedded roots (tests only).
pub trusted_pem: Option<Vec<u8>>,
/// Where each delivered entry is also kept, so a later delivery can put back one another writer overwrote.
pub spool: Option<PathBuf>,
}
/// What:
/// The options the script's environment variables give for a repository: the log under `repo_root` unless
/// `SHADOW_REVIEW_LOG` names another, and the first credential variable that is set, even to an empty text.
/// Why:
/// `??` in the script falls back only for an unset variable, so an empty first key does not fall back.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const logPath = process.env.SHADOW_REVIEW_LOG ?? `${repo}/doc/audit/standards-review-gate-shadow.md`;
/// ```
pub fn options_from_environment(repo_root: &Path, variable: &dyn Fn(&str) -> Option<String>) -> ReviewOptions {
let log_path: PathBuf = variable(LOG_VARIABLE).map_or_else(|| return repo_root.join(LOG_RELATIVE), PathBuf::from);
let api_key: Option<String> = variable(KEY_VARIABLES[0]).or_else(|| return variable(KEY_VARIABLES[1]));
return ReviewOptions { log_path, api_key, endpoint: String::from(ENDPOINT), deadline: SCRIPT_DEADLINE, trusted_pem: None, spool: None };
}
/// What:
/// Whether the environment asks for the full-review route, which this port leaves out.
/// Why:
/// The caller says so in a notice instead of silently running the cheap route.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// process.env.SHADOW_REVIEW_ALLOW === '1'
/// ```
pub fn full_review_requested(variable: &dyn Fn(&str) -> Option<String>) -> bool {
return variable(ALLOW_VARIABLE).as_deref() == Some("1");
}
/// What:
/// How a run ended.
/// Why:
/// Callers log it; none of it fails a landing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReviewOutcome = 'no-entry' | 'skipped' | { reviewed: Judged } | { unread: string } | { unwritable: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReviewOutcome {
/// No names, or only the log: no entry, as the script exits silently.
NoEntry,
/// The no-code-files entry was appended.
Skipped,
/// A review entry was appended; the gateway's verdict, or its failure in `outcome` with an empty `top`.
Reviewed(Judged),
/// The commit could not be read; no entry.
Unread(String),
/// The entry could not be appended.
Unwritable(String),
/// The log already names this commit (a repeated item): nothing was asked and nothing was added.
AlreadyLogged,
}
/// What:
/// The gateway step: the verdict, or the failure message, and the elapsed milliseconds.
/// Why:
/// A missing credential fails before any connection, as in the script.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// try { ...fetch... } catch (error) { lunaOutcome = 'Luna failed: ' + message }
/// ```
fn ask_gateway(input: &ReviewInput, options: &ReviewOptions) -> Judged {
let failed = |message: String| return Judged { top: String::new(), outcome: format!("Luna failed: {message}") };
let key: &str = options.api_key.as_deref().unwrap_or("");
if key.is_empty() {
return failed(String::from(NO_KEY_MESSAGE));
}
let gateway: Gateway = Gateway { url: options.endpoint.as_str(), api_key: key, deadline: options.deadline, trusted_pem: options.trusted_pem.as_deref() };
let reply: Result<Reply, String> = post_decision(&gateway, request_body(input.model_input.as_str()).as_str());
return match reply.and_then(|answer| return read_answer(answer.status, answer.body.as_str())) {
Ok(judged) => judged,
Err(message) => failed(message),
};
}
/// What:
/// Review one landed commit: `names` are the changed names the landing knows, `read` fetches the commit's patch and
/// files (called only for a commit with code files), `now_millis` is the clock.
/// Why:
/// One function is the script's whole body, with the reads and the clock as parameters so tests fix both.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function reviewLanded({ names, sha, subject, read, options, nowMillis }): Promise<ReviewOutcome>;
/// ```
pub fn review_landed(
names: &[String],
sha: &str,
subject: &str,
read: &dyn Fn(&[String]) -> Result<LandedCommit, String>,
options: &ReviewOptions,
now_millis: &dyn Fn() -> u64,
) -> ReviewOutcome {
let code_files: Vec<String> = match plan_for(names) {
Plan::Silent => return ReviewOutcome::NoEntry,
Plan::NoCode => {
return match deliver(options.log_path.as_path(), options.spool.as_deref(), sha, render_skipped(sha, subject).as_str()) {
Ok(_) => ReviewOutcome::Skipped,
Err(message) => ReviewOutcome::Unwritable(message),
};
}
Plan::Review(files) => files,
};
if names_commit(options.log_path.as_path(), sha) {
return ReviewOutcome::AlreadyLogged;
}
let commit: LandedCommit = match read(code_files.as_slice()) {
Ok(read_commit) => read_commit,
Err(message) => return ReviewOutcome::Unread(message),
};
let input: ReviewInput = build_input(&commit, code_files.as_slice());
let started: u64 = now_millis();
let judged: Judged = ask_gateway(&input, options);
let finished: u64 = now_millis();
let entry: ReviewEntry = ReviewEntry {
date: format_iso_milliseconds(i64::try_from(finished).unwrap_or(i64::MAX)),
sha: String::from(sha),
subject: String::from(subject),
code_files: input.code_file_count,
diff_lines: input.diff_line_count,
truncated: input.truncated,
input_units: input.model_input_units,
seconds: seconds_with_tenth(finished.saturating_sub(started)),
top: judged.top.clone(),
outcome: judged.outcome.clone(),
};
return match deliver(options.log_path.as_path(), options.spool.as_deref(), sha, render_review(&entry).as_str()) {
Ok(_) => ReviewOutcome::Reviewed(judged),
Err(message) => ReviewOutcome::Unwritable(message),
};
}
/// What:
/// Run `job` on a helper thread and wait at most `limit`; `None` means the time ran out and the job was abandoned.
/// An abandoned job keeps running until the process exits, which is the end of the wrapper command.
/// Why:
/// This is the hard bound over the Git reads, the call and the log write together: the landing's caller waits `limit`
/// at most. A job that panics also gives `None`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await Promise.race([job(), wait(limit).then(() => undefined)])
/// ```
pub fn run_within<T: Send + 'static>(limit: Duration, job: impl FnOnce() -> T + Send + 'static) -> Option<T> {
let (sender, receiver) = mpsc::channel::<T>();
let worker = std::thread::Builder::new().name(String::from("standards-review")).spawn(move || {
let _ = sender.send(job());
});
if worker.is_err() {
return None;
}
return receiver.recv_timeout(limit).ok();
}
/// Controls stay out of the release executable.
#[cfg(test)]
#[path = "standards_review_run_tests.rs"]
mod tests;