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
//! What:
//! The pure part of the standards-review shadow run: which landed commits get an entry, and the capped model input
//! built from the commit's patch and the excerpts of its changed code files.
//! Why:
//! This is the root `mise.toml` task `shadow-review` after its Git reads: the no-entry, no-code-files and review
//! cases, the 2000-line patch cap, the eight-file and 4000-line excerpt caps, and the 24000-unit input cap. Taking
//! the patch and the file contents as plain values lets the caps be tested without Git, a network or a clock.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const plan = classify(names); const input = buildReviewInput({ commit });
//! ```
use super::standards_review_patch::{NewSpan, new_spans};
use super::standards_review_text::{utf16_length, utf16_prefix};
use std::collections::BTreeMap;
/// The shadow log's repository-relative path; a commit that changes nothing else is the hook's own record.
pub const LOG_RELATIVE: &str = "doc/audit/standards-review-gate-shadow.md";
/// Patch lines kept for the model.
pub const CAP_LINES: usize = 2000;
/// Code files that get an excerpt.
pub const MAX_EXCERPT_FILES: usize = 8;
/// Lines of context kept around the changed span of an excerpted file.
pub const EXCERPT_CONTEXT: i64 = 30;
/// Excerpt lines after which no further file gets an excerpt.
pub const EXCERPT_LINE_CAP: i64 = 4000;
/// Lines of the head of a file that has no changed line to anchor an excerpt on: the context on both sides and one.
pub const HEAD_WINDOW: i64 = 2 * EXCERPT_CONTEXT + 1;
/// UTF-16 units of model input.
pub const INPUT_UNIT_CAP: usize = 24000;
/// Name endings that count as code.
pub const CODE_SUFFIXES: [&str; 6] = [".ts", ".tsx", ".rs", ".js", ".mjs", ".cjs"];
/// What:
/// What a landed commit's changed names say before anything is read from Git.
/// Why:
/// A commit with no names (a root commit, a merge) or only the log gets no entry, and a commit with no code gets a
/// three-line skip entry; neither needs the patch or any file, so neither costs a Git start.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type Plan = { kind: 'silent' } | { kind: 'no-code' } | { kind: 'review'; codeFiles: string[] };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Plan {
/// No entry at all.
Silent,
/// The skip entry.
NoCode,
/// A review of these code files, in name order.
Review(Vec<String>),
}
/// What:
/// The plan for the names `git diff-tree --name-only -r` prints for the landed commit.
/// Why:
/// It is the script's first three branches, in order.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// if (names.every((name) => name === logRelative)) return silent; if (codeFiles.length === 0) return noCode;
/// ```
pub fn plan_for(names: &[String]) -> Plan {
if names.iter().all(|name| return name == LOG_RELATIVE) {
return Plan::Silent;
}
let code_files: Vec<String> = names
.iter()
.filter(|name| return CODE_SUFFIXES.iter().any(|suffix| return name.ends_with(suffix)))
.cloned()
.collect();
if code_files.is_empty() {
return Plan::NoCode;
}
return Plan::Review(code_files);
}
/// What:
/// Everything the review reads about one landed commit: the patch, and the new-side text of each code file that
/// will be excerpted. A path missing from `contents` has no new-side file (it was deleted).
/// Why:
/// One value is all the pure builder needs, whichever way the reads were made.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type LandedCommit = { sha: string; subject: string; patch: string; contents: Map<string, string> };
/// ```
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LandedCommit {
/// Full commit name.
pub sha: String,
/// Subject line, trimmed.
pub subject: String,
/// `git show --format= --patch` text.
pub patch: String,
/// New-side text by path.
pub contents: BTreeMap<String, String>,
}
/// What:
/// The sizes the entry reports and the text the model receives.
/// Why:
/// `model_input` is cut at the unit cap, while `model_input_units` is the length the script prints: the cut length.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReviewInput = { codeFileCount: number; diffLineCount: number; truncated: boolean; modelInput: string; modelInputUnits: number };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReviewInput {
/// Code files in the commit.
pub code_file_count: usize,
/// Lines of the whole patch (a final newline counts as one more, empty, line).
pub diff_line_count: usize,
/// Whether the patch is over `CAP_LINES`.
pub truncated: bool,
/// The text sent to the model.
pub model_input: String,
/// The reported length of `model_input` in UTF-16 units.
pub model_input_units: usize,
}
/// What:
/// The excerpt of one file: its header and the text of the window around the changed span.
/// The window runs from `EXCERPT_CONTEXT` lines before the first changed new-side line to `EXCERPT_CONTEXT` lines
/// after the last, cut from the new-side file. A file with no changed line to anchor on (a binary file, a mode change,
/// a pure rename) gets the head of the file instead.
/// Why:
/// Returns the window's line count too, which the 4000-line cap adds up. The script cut the window from old-side
/// numbers, which sit in the wrong place after an earlier insertion, and gave a file without hunks a nonsensical
/// negative window.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// excerptParts.push(`// candidate excerpt of ${name}, lines ${from} to ${to} of ${fileLines.length}`);
/// ```
fn excerpt(name: &str, content: &str, spans: &[NewSpan]) -> (Vec<String>, i64) {
let file_lines: Vec<&str> = content.split('\n').collect();
let total: i64 = i64::try_from(file_lines.len()).unwrap_or(i64::MAX);
let first_changed: Option<i64> = spans.iter().map(|span| return i64::try_from(span.first).unwrap_or(i64::MAX)).min();
let last_changed: Option<i64> = spans.iter().map(|span| return i64::try_from(span.last).unwrap_or(i64::MAX)).max();
let (from, to) = match (first_changed, last_changed) {
(Some(first), Some(last)) => (1.max(first.saturating_sub(EXCERPT_CONTEXT)), total.min(last.saturating_add(EXCERPT_CONTEXT))),
_ => (1, total.min(HEAD_WINDOW)),
};
let header: String = format!("// candidate excerpt of {name}, lines {from} to {to} of {total}");
let begin: usize = usize::try_from(from - 1).unwrap_or(0);
let end: usize = usize::try_from(to).unwrap_or(0);
let body: String = if begin < end { file_lines[begin..end].join("\n") } else { String::new() };
return (vec![header, body], to - from + 1);
}
/// What:
/// Build the capped model input for a reviewed commit.
/// Why:
/// The input is the first `CAP_LINES` patch lines, a blank line, then each excerpt separated by blank lines, cut to
/// `INPUT_UNIT_CAP` UTF-16 units.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const lunaInput = (lines.slice(0, capLines).join('\n') + '\n\n' + excerptParts.join('\n\n')).slice(0, 24000);
/// ```
pub fn build_input(commit: &LandedCommit, code_files: &[String]) -> ReviewInput {
let lines: Vec<&str> = commit.patch.split('\n').collect();
let kept: &[&str] = &lines[..lines.len().min(CAP_LINES)];
let mut parts: Vec<String> = Vec::new();
let mut excerpt_lines: i64 = 0;
for name in code_files.iter().take(MAX_EXCERPT_FILES) {
if excerpt_lines >= EXCERPT_LINE_CAP {
break;
}
let Some(content) = commit.contents.get(name) else {
continue;
};
let spans: Vec<NewSpan> = new_spans(lines.as_slice(), name);
let (pieces, covered) = excerpt(name, content, spans.as_slice());
parts.extend(pieces);
excerpt_lines += covered;
}
let full: String = format!("{}\n\n{}", kept.join("\n"), parts.join("\n\n"));
let units: usize = utf16_length(full.as_str()).min(INPUT_UNIT_CAP);
return ReviewInput {
code_file_count: code_files.len(),
diff_line_count: lines.len(),
truncated: lines.len() > CAP_LINES,
model_input: utf16_prefix(full.as_str(), INPUT_UNIT_CAP),
model_input_units: units,
};
}
/// Controls stay out of the release executable.
#[cfg(test)]
#[path = "standards_review_input_tests.rs"]
mod tests;