Skip to main content

git_policy_cli/
diagnostic_log.rs

1//! What:
2//!  Line-oriented debug and warning diagnostics on standard error,
3//!  switched like the
4//!       incumbent's console sink.
5//! Why:
6//!  The incumbent prints debug records only when `MONOCHROMATIC_VERBOSE=true` and warnings
7//!      unless `MONOCHROMATIC_WARN=false` (`package/module/logger/src/sink/console.ts`),
8//!  and its
9//!      spec requires that diagnostics never corrupt a JSONL stream:
10//!  each diagnostic is one
11//!      complete line written in one call (`SPEC.md`,
12//!  "Debug logging").
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! const l = tagged({ tag: 'cli-git' }); l.debug(message); l.warn(message);
17//! ```
18
19/// The current time in `toISOString` form.
20use super::iso_time::now_iso;
21/// The trait that gives output streams `.write_all(..)`.
22use std::io::Write;
23
24/// What:
25///  Whether debug diagnostics are on:
26///  `MONOCHROMATIC_VERBOSE` is exactly `true`.
27/// Why:
28///   The switch is shared with the TypeScript tools (decision recorded in
29///       `doc/handover/cli-git-rust-implementation.md`,
30///  "Adopted without a question").
31///
32/// In TS you'd write (pseudocode):
33/// ```ts
34/// process.env.MONOCHROMATIC_VERBOSE === 'true'
35/// ```
36pub fn verbose_enabled() -> bool {
37    return std::env::var_os("MONOCHROMATIC_VERBOSE").is_some_and(is_true);
38}
39
40/// What:
41///  Whether an environment value is exactly `true`.
42/// Why:
43///   A named function keeps the check out of a closure.
44///
45/// In TS you'd write (pseudocode):
46/// ```ts
47/// (value: string) => value === 'true'
48/// ```
49fn is_true(value: std::ffi::OsString) -> bool {
50    return value == "true";
51}
52
53/// What:
54///  Whether warnings are suppressed:
55///  `MONOCHROMATIC_WARN` is exactly `false`.
56/// Why:
57///   Machine-protocol consumers silence warnings this way in the incumbent.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// process.env.MONOCHROMATIC_WARN === 'false'
62/// ```
63pub fn warnings_suppressed() -> bool {
64    return std::env::var_os("MONOCHROMATIC_WARN").is_some_and(is_false);
65}
66
67/// What:
68///  Whether an environment value is exactly `false`.
69/// Why:
70///   A named function keeps the check out of a closure.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// (value: string) => value === 'false'
75/// ```
76fn is_false(value: std::ffi::OsString) -> bool {
77    return value == "false";
78}
79
80/// What:
81///  One diagnostic line in the incumbent's console format,
82///       `[<level>] [<ISO time>] [cli-git] [<tag>] <message>`,
83///  with every control character of
84///       the message replaced by a space.
85/// Why:
86///   The incumbent's tagged loggers print `[warn] [2026-10-06T21:40:39.524Z] [cli-git] ...`;
87///       one line per record keeps standard error parseable beside JSONL events.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// `[${level}] [${new Date().toISOString()}] [cli-git] [${tag}] ${neutralize(message)}\n`
92/// ```
93pub fn diagnostic_line(level: &str, time: &str, tag: &str, message: &str) -> String {
94    // `mut` allows building the neutralized message one character at a time.
95    let mut single: String = String::with_capacity(message.len());
96    for character in message.chars() {
97        if character.is_control() {
98            single.push(' ');
99        } else {
100            single.push(character);
101        }
102    }
103    return format!("[{level}] [{time}] [cli-git] [{tag}] {single}\n");
104}
105
106/// What:
107///  Write one complete line to standard error,
108///  ignoring a closed stream.
109/// Why:
110///   A diagnostic must never turn into a failure of the command it describes.
111///
112/// In TS you'd write (pseudocode):
113/// ```ts
114/// process.stderr.write(line);
115/// ```
116fn write_line(line: &str) {
117    // `let _ =` discards the write result: there is nowhere left to report it.
118    let _ = std::io::stderr().write_all(line.as_bytes());
119}
120
121/// What:
122///  Print a debug diagnostic when verbose diagnostics are on.
123///       `tag` names the function or module boundary,
124///  as the incumbent's tagged loggers do.
125/// Why:
126///   Decisions such as "owner alive,
127///  skipping" are invisible otherwise.
128///
129/// In TS you'd write (pseudocode):
130/// ```ts
131/// tagged({ tag }).debug(message);
132/// ```
133pub fn debug(tag: &str, message: &str) {
134    if verbose_enabled() {
135        write_line(diagnostic_line("debug", now_iso().as_str(), tag, message).as_str());
136    }
137}
138
139/// What:
140///  Print a warning unless warnings are suppressed.
141/// Why:
142///   Conditions the user should know about that do not fail the command,
143///  such as a
144///       landed-capture record that could not be written.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// tagged({ tag }).warn(message);
149/// ```
150pub fn warn(tag: &str, message: &str) {
151    if !warnings_suppressed() {
152        write_line(diagnostic_line("warn", now_iso().as_str(), tag, message).as_str());
153    }
154}
155
156/// Line formatting controls stay out of the release executable.
157#[cfg(test)]
158#[path = "diagnostic_log_tests.rs"]
159mod tests;