Skip to main content

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