Skip to main content

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;