1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// SPDX-License-Identifier: MIT OR Apache-2.0
// Error-envelope emitters, split out of `output.rs` so the writer stays below
// the 500-line ceiling.
/// Write a structured error as NDJSON directly to a raw writer.
///
/// # Errors
///
/// Returns an I/O error if writing the error JSON to the underlying writer fails.
#[cold]
pub fn write_error_json(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
) -> anyhow::Result<()> {
write_error_json_with_context(out, err, path, &ErrorContext::default())
}
/// Write a structured error as NDJSON directly to a raw writer, with context.
///
/// # Errors
///
/// Returns an I/O error if writing the error JSON to the underlying writer fails.
#[cold]
pub fn write_error_json_with_context(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
ctx: &ErrorContext,
) -> anyhow::Result<()> {
write_error_json_shaped(out, err, path, ctx, &OutputShape::default())
}
/// Write a structured error applying agent-native optional `--select` projection.
///
/// Used by `main` so failure envelopes stay on the same stdout contract as
/// success events. **`--filter` never drops errors** (agent-native always-emit);
/// **`--limit` does not apply** on this path. Only `--select`/`--fields` may
/// thin top-level keys.
///
/// # Errors
///
/// Returns an I/O error if writing fails.
#[cold]
pub fn write_error_json_shaped(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
ctx: &ErrorContext,
shape: &OutputShape,
) -> anyhow::Result<()> {
write_error_json_shaped_with_target(out, err, path, ctx, shape, None)
}
/// Like [`write_error_json_shaped`], also naming the subject the run did not reach.
///
/// A verb with side effects stamps `target_resolved` and `target_source` on its
/// terminal record. When the run fails before writing one, the failure envelope
/// IS the terminal record, so the same two keys belong on it: otherwise the
/// caller cannot tell which subject was left untouched.
///
/// # Errors
///
/// Returns an I/O error if writing fails.
#[cold]
pub fn write_error_json_shaped_with_target(
out: &mut impl Write,
err: &AtomwriteError,
path: Option<&Path>,
ctx: &ErrorContext,
shape: &OutputShape,
target: Option<(&serde_json::Value, &str)>,
) -> anyhow::Result<()> {
let mut json = ErrorJson::from_error_with_context(err, ctx);
if json.path.is_none() {
json.path = path.map(|p| p.display().to_string());
}
// The error channel carries the same credential surface as the success
// channel, and it reaches stdout through THIS function — not through the
// writer's `write_error_with_context`. Both branches therefore run the
// unconditional key floor and the opt-in content redaction, so a credential
// in a path or a message is scrubbed whether or not a projection flag is
// set. Before this, the branch without `--select` serialized the envelope
// straight from the struct, so `--redact` was inert here while the same
// credential in a success payload was redacted.
let mut value = serde_json::to_value(&json)?;
if let Some((resolved, source)) = target {
stamp_target_keys(&mut value, resolved, source, true);
}
let shaped = shape_error_value(value, shape);
serde_json::to_writer(&mut *out, &shaped).map_err(io::Error::from)?;
out.write_all(b"\n")?;
out.flush()?;
Ok(())
}