spec_driven_docs/output.rs
1//! The output boundary: every byte the binary prints goes through here.
2//!
3//! stdout carries the result — findings, summaries, rendered documents —
4//! as plain text, which is what pre-commit shows a consumer. stderr carries
5//! exactly one JSON error envelope for operational failures. No other
6//! module (main.rs aside) prints.
7
8use std::fmt::Display;
9
10use crate::domain::finding::Finding;
11use crate::error::AppError;
12
13/// Print one result line to stdout.
14pub fn line(text: impl Display) {
15 println!("{text}");
16}
17
18/// Write pre-rendered bytes — completions, man pages — to stdout.
19pub fn raw(bytes: &[u8]) {
20 use std::io::Write;
21 let _ = std::io::stdout().write_all(bytes);
22}
23
24/// Print every finding, one line each, to stdout.
25pub fn findings(findings: &[Finding]) {
26 for finding in findings {
27 println!("{finding}");
28 }
29}
30
31/// Emit the one structured error envelope to stderr.
32///
33/// Violations carry no envelope: their findings are already on stdout and
34/// the exit code is the report.
35pub fn error_envelope(error: &AppError) {
36 if matches!(error, AppError::Violations { .. }) {
37 return;
38 }
39 let envelope = serde_json::json!({
40 "ok": false,
41 "error": {
42 "kind": error.kind(),
43 "message": error.to_string(),
44 "exit_code": error.exit_code(),
45 }
46 });
47 eprintln!("{envelope}");
48}