git_policy_cli/standards_review_text.rs
1//! What:
2//! The few text behaviors of the standards-review log that JavaScript defines and Rust does not share:
3//! UTF-16 lengths and prefixes, `Number` to text, `toFixed(1)` of a duration, and Git's `%s` subject.
4//! Why:
5//! The root `mise.toml` task `shadow-review` measures and cuts text in UTF-16 code units and prints numbers the way
6//! ECMAScript does, and its log is compared entry by entry with this port's, so the same text must give the same
7//! counts and the same bytes.
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! text.length; text.slice(0, limit); String(number); (milliseconds / 1000).toFixed(1); // git log --format=%s
12//! ```
13
14/// What:
15/// The length of `text` in UTF-16 code units, which is what JavaScript's `.length` counts.
16/// `usize` is the unsigned size every length uses (siblings `u32`, `u64`).
17/// Why:
18/// The log prints `luna input N chars` with that count, and a Rust `.len()` would count UTF-8 bytes instead.
19///
20/// In TS you'd write (pseudocode):
21/// ```ts
22/// text.length
23/// ```
24pub fn utf16_length(text: &str) -> usize {
25 return text.chars().map(char::len_utf16).sum();
26}
27
28/// What:
29/// The first `limit` UTF-16 code units of `text`, as JavaScript's `text.slice(0, limit)` keeps them.
30/// A cut that falls inside a surrogate pair drops the first half, because a Rust `String` cannot hold half a pair.
31/// Why:
32/// The model input and the failure excerpt are cut by code units; a character that a cut would split is the only
33/// place where the result differs from the script's, and the script would send a lone surrogate there.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// text.slice(0, limit)
38/// ```
39pub fn utf16_prefix(text: &str, limit: usize) -> String {
40 let mut kept: String = String::new();
41 let mut used: usize = 0;
42 for character in text.chars() {
43 let width: usize = character.len_utf16();
44 if used + width > limit {
45 break;
46 }
47 kept.push(character);
48 used += width;
49 }
50 return kept;
51}
52
53/// What:
54/// `value` as ECMAScript's `String(value)` prints a finite number: shortest digits, no exponent between 1e-6 and
55/// 1e21, otherwise `1e-7` or `1.5e+21` style.
56/// Why:
57/// Rust prints `0.0000001` where JavaScript prints `1e-7`, and the log carries the gateway's probabilities as
58/// `Number` text.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// String(value)
63/// ```
64pub fn js_number_text(value: f64) -> String {
65 if value == 0.0 {
66 return String::from("0");
67 }
68 let sign: &str = if value < 0.0 { "-" } else { "" };
69 // Rust's `{:e}` prints the shortest digits that round-trip, as `d.ddde-5`.
70 let scientific: String = format!("{:e}", value.abs());
71 let (mantissa, exponent_text) = scientific.split_once('e').unwrap_or((scientific.as_str(), "0"));
72 let digits: String = mantissa.replace('.', "");
73 let exponent: i32 = exponent_text.parse::<i32>().unwrap_or(0);
74 let count: i32 = i32::try_from(digits.len()).unwrap_or(i32::MAX);
75 // ECMAScript's `n`: the decimal point sits after the first `n` digits.
76 let point: i32 = exponent + 1;
77 let body: String = if count <= point && point <= 21 {
78 format!("{digits}{}", "0".repeat(usize::try_from(point - count).unwrap_or(0)))
79 } else if 0 < point && point <= 21 {
80 let split: usize = usize::try_from(point).unwrap_or(0);
81 format!("{}.{}", &digits[..split], &digits[split..])
82 } else if -6 < point && point <= 0 {
83 format!("0.{}{digits}", "0".repeat(usize::try_from(-point).unwrap_or(0)))
84 } else {
85 let power: i32 = point - 1;
86 let power_sign: &str = if power < 0 { "-" } else { "+" };
87 let lead: &str = &digits[..1];
88 let rest: &str = &digits[1..];
89 if rest.is_empty() {
90 format!("{lead}e{power_sign}{}", power.abs())
91 } else {
92 format!("{lead}.{rest}e{power_sign}{}", power.abs())
93 }
94 };
95 return format!("{sign}{body}");
96}
97
98/// What:
99/// `(milliseconds / 1000).toFixed(1)`: seconds with one decimal, a tie rounded up as JavaScript rounds it.
100/// Why:
101/// The log prints the review's wall-clock cost that way. Only a value that is exactly a quarter second ties
102/// (250 ms past a half second), and Rust rounds a tie to even where JavaScript takes the larger neighbor.
103///
104/// In TS you'd write (pseudocode):
105/// ```ts
106/// (milliseconds / 1000).toFixed(1)
107/// ```
108pub fn seconds_with_tenth(milliseconds: u64) -> String {
109 const TIE_PERIOD_MS: u64 = 500;
110 const TIE_REMAINDER_MS: u64 = 250;
111 const TENTH_MS: u64 = 100;
112 if milliseconds % TIE_PERIOD_MS == TIE_REMAINDER_MS {
113 let tenths: u64 = milliseconds / TENTH_MS + 1;
114 return format!("{}.{}", tenths / 10, tenths % 10);
115 }
116 let seconds: f64 = milliseconds as f64 / 1000.0;
117 return format!("{seconds:.1}");
118}
119
120/// What:
121/// The subject Git's `%s` prints for a commit message, trimmed as the script trims it: blank lines skipped, the
122/// first paragraph's lines joined by one space, each line without trailing whitespace.
123/// Why:
124/// The landing holds the commit message, so the subject needs no `git log` start.
125///
126/// In TS you'd write (pseudocode):
127/// ```ts
128/// execFileSync('git', ['log', '-1', '--format=%s', sha]).trim()
129/// ```
130pub fn commit_subject(message: &str) -> String {
131 let mut paragraph: Vec<&str> = Vec::new();
132 for line in message.split('\n') {
133 let trimmed: &str = line.trim_end();
134 if trimmed.trim_start().is_empty() {
135 if paragraph.is_empty() {
136 continue;
137 }
138 break;
139 }
140 paragraph.push(trimmed);
141 }
142 return paragraph.join(" ").trim().to_string();
143}
144
145/// What:
146/// `text` on one line: every control character and Unicode line or paragraph separator becomes a visible escape
147/// (`\n`, `\r`, `\t`, otherwise `\u{hex}`), and a backslash stays itself.
148/// Why:
149/// The log is line-structured and a reader parses it by lines. A response body, a transport error or a model answer
150/// that carried a line break could otherwise start a fake entry. The script wrote such text unchanged.
151///
152/// In TS you'd write (pseudocode):
153/// ```ts
154/// text.replace(/[\u0000-\u001f\u007f-\u009f
]/g, (c) => escapeVisible(c))
155/// ```
156pub fn on_one_line(text: &str) -> String {
157 let mut line: String = String::with_capacity(text.len());
158 for character in text.chars() {
159 match character {
160 '\n' => line.push_str("\\n"),
161 '\r' => line.push_str("\\r"),
162 '\t' => line.push_str("\\t"),
163 other if other.is_control() || other == '\u{2028}' || other == '\u{2029}' => {
164 line.push_str(&format!("\\u{{{:x}}}", u32::from(other)));
165 }
166 other => line.push(other),
167 }
168 }
169 return line;
170}
171
172/// Controls stay out of the release executable.
173#[cfg(test)]
174#[path = "standards_review_text_tests.rs"]
175mod tests;