tatara_process/anyhow_flatten.rs
1//! Substrate primitive over `anyhow::Result<T>` — the ONE substrate
2//! owner of the `.map_err(|e| anyhow::anyhow!("<ctx>: {e}"))` wrap-
3//! shape every reconciler consumer restates by hand at the
4//! anyhow-returning → anyhow-returning display-prefix boundary. Peer
5//! of [`crate::kube_error::KubeResultExt`] and
6//! [`crate::hostname::HostnameResultExt`] on the flatten-wrap axis; the
7//! three traits partition the display-prefix wrap space by underlying
8//! error type — [`kube::Error`] on the K8s peer, [`crate::hostname::
9//! HostnameError`] on the hostname peer, [`anyhow::Error`] on this
10//! peer (the "already an anyhow error, we just want a static or
11//! `format!`-composed slug in front of its `Display` output" case).
12//!
13//! Pre-lift the shape was hand-authored at FIVE `phase_machine.rs`
14//! sites in `tatara-reconciler` past the ★★ PRIME-DIRECTIVE ≥ 2
15//! duplication threshold, each restating the SAME closure — capture
16//! an [`anyhow::Error`] returned by a downstream primitive
17//! ([`crate::boundary::check_depends_on`],
18//! [`crate::render::render_routing`],
19//! [`crate::boundary::evaluate`],
20//! [`crate::render::render_export_jobs`],
21//! [`crate::ssapply::apply_owned`]), prepend a context slug identifying
22//! which primitive faulted, delegate the tail to [`anyhow::Error`]'s
23//! `Display` impl via the `{e}` slot — differing only in the context
24//! slug prefix each callsite stamped.
25//!
26//! Post-lift each callsite reads
27//! `<anyhow-returning-call>().await.flatten_ctx("<slug>")?` (or the
28//! owned-`String` peer for consumers that compose the slug via
29//! `format!`) and the wrap-shape lives at ONE substrate owner here.
30//! The composed [`anyhow::Error`]'s `Display` is byte-identical to
31//! the pre-lift chain (`format!("{ctx}: {e}")`, threading the source
32//! [`anyhow::Error`]'s own `Display` verbatim into the `{e}` slot),
33//! so operator-facing log output and any error-chain greps still
34//! match bytewise. A regression that drifts the separator, swaps
35//! the two slots, or wraps the source [`anyhow::Error`] with a
36//! chain-form `source` (which would change `Display` output on the
37//! `err` slot when downstream consumers format with `{e}` rather
38//! than `{e:#}`) surfaces at the tests below rather than as silent
39//! operator-facing drift across the five pre-lift consumers.
40//!
41//! ### Naming — `flatten_ctx`, not `anyhow::Context::context`
42//!
43//! Same discipline as [`crate::kube_error::KubeResultExt::kube_ctx`]
44//! and [`crate::hostname::HostnameResultExt::hostname_ctx`] — but with
45//! a more urgent motivation, because THIS trait operates on the SAME
46//! `anyhow::Result<T>` type [`anyhow::Context::context`] takes.
47//! `anyhow::Context::context` wraps the source in a chain (so
48//! `Display` emits ONLY the context slug and callers reach the source
49//! [`anyhow::Error`] via [`std::error::Error::source`] traversal, or
50//! by formatting with the alternate `{:#}` specifier that walks the
51//! chain), while this trait's `flatten_ctx` FLATTENS to a display-
52//! prefix shape (`"<ctx>: <anyhow::Error display>"`) — the pre-lift
53//! wire format every consumer's `tracing::error!(error = %e, ...)`
54//! log line already encoded. Sharing the name (`context`) would let
55//! a caller who has [`anyhow::Context`] in scope resolve to the
56//! WRONG method (a chain-wrap instead of the display-prefix flatten)
57//! and silently drop the underlying error detail from every
58//! reconciler-error tracing span whose formatter interpolates `{e}`.
59//!
60//! ### Two flavors: `flatten_ctx` + `flatten_ctx_with`
61//!
62//! * [`FlattenCtxExt::flatten_ctx`] takes a `&'static str` context —
63//! the most common shape, matching every static-slug consumer
64//! (`"depends_on check"`, `"render routing"`, `"render export
65//! jobs"`, `"apply export job"`). Static binding keeps the
66//! compile-time contract that the context slug is a bare literal,
67//! no allocation, no dynamic content leaking into an error stream
68//! downstream operators grep on.
69//! * [`FlattenCtxExt::flatten_ctx_with`] takes an owned [`String`]
70//! context — the escape hatch for the one dynamic-slug consumer
71//! (`format!("evaluate {:?}", c.kind)`, the `ConditionKind`
72//! variant name only known at runtime), matching the pre-lift
73//! shape where a `format!` composed the slug per-call.
74//!
75//! ### `#[must_use]`
76//!
77//! Every consumer threads the `?` short-circuit onto its handler's
78//! `Result<_, anyhow::Error>` return — dropping the wrap swallows
79//! the underlying failure entirely, which is never the intended
80//! semantic at any of the five pre-lift consumers.
81//!
82//! Theory anchor: THEORY.md §VI.1 (generation over composition — the
83//! anyhow-with-display-prefix wrap-shape recurred at five hand-
84//! authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
85//! trigger, and is lifted to ONE substrate owner here). THEORY.md
86//! §II.1 invariant 5 (composition preserves proofs — a regression
87//! that drifts the display-prefix separator or the byte-shape at
88//! ONE site surfaces here at the substrate pin rather than as
89//! silent operator-facing skew across every reconciler phase tick).
90
91/// Substrate extension trait over `anyhow::Result<T>` — the ONE
92/// substrate owner of the `.map_err(|e| anyhow::anyhow!("<ctx>: {e}"))`
93/// display-prefix wrap-shape for consumers whose source error is
94/// already `anyhow::Error`. See the module docs for the full callsite
95/// audit + the naming rationale (why `flatten_ctx` and not
96/// `anyhow::Context::context`).
97pub trait FlattenCtxExt<T>: Sized {
98 /// Wrap the source [`anyhow::Error`] (if any) with a static
99 /// context prefix, producing an [`anyhow::Result`] whose error
100 /// `Display` reads exactly `"<context>: <source display>"`.
101 #[must_use = "an error wrap that isn't threaded via `?` swallows the underlying anyhow failure"]
102 fn flatten_ctx(self, context: &'static str) -> anyhow::Result<T>;
103
104 /// Owned-string peer of [`Self::flatten_ctx`] — the escape hatch
105 /// for consumers that compose the context slug via `format!`
106 /// (e.g. `format!("evaluate {:?}", c.kind)` where the tail is
107 /// only known at runtime).
108 #[must_use = "an error wrap that isn't threaded via `?` swallows the underlying anyhow failure"]
109 fn flatten_ctx_with(self, context: String) -> anyhow::Result<T>;
110}
111
112impl<T> FlattenCtxExt<T> for anyhow::Result<T> {
113 #[inline]
114 fn flatten_ctx(self, context: &'static str) -> anyhow::Result<T> {
115 self.map_err(|e| anyhow::anyhow!("{context}: {e}"))
116 }
117
118 #[inline]
119 fn flatten_ctx_with(self, context: String) -> anyhow::Result<T> {
120 self.map_err(|e| anyhow::anyhow!("{context}: {e}"))
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn sample_err() -> anyhow::Error {
129 anyhow::anyhow!("underlying primitive failed: bad slot 42")
130 }
131
132 // ─── FlattenCtxExt::flatten_ctx substrate pins ───────────────────
133 //
134 // Fail-before-pass-after granularity: the `FlattenCtxExt::flatten_ctx`
135 // trait method did not exist before this commit, so each test below
136 // fails to compile pre-lift. Post-lift they collectively pin the
137 // display-prefix wrap-shape at ONE substrate owner — a regression
138 // that drifts the separator, swaps the two slots, wraps the source
139 // anyhow error with a chain-form `source` (which would change
140 // `Display` output when downstream tracing formatters interpolate
141 // `{e}` rather than the chain-walking `{e:#}`), or promotes the
142 // pass-through arm to a synthesis (an empty `Ok(())`, a mutated
143 // context slug) surfaces HERE rather than as silent operator-facing
144 // skew across the five `phase_machine.rs` pre-lift consumers whose
145 // log output already encoded the flat `"<ctx>: <anyhow display>"`
146 // shape.
147
148 #[test]
149 fn flatten_ctx_static_str_context_matches_pre_lift_format_bytewise() {
150 // Byte-shape parity pin: the wrap output of `flatten_ctx
151 // ("<slug>")` MUST be `Display`-identical to the pre-lift
152 // hand-authored `.map_err(|e| anyhow!("<slug>: {e}"))` chain.
153 // A regression that inserted a separator character (`"<slug>::
154 // <anyhow>"`), dropped the space after the colon, or swapped
155 // the two slots (`"<anyhow>: <slug>"`) surfaces HERE rather
156 // than as silent drift at every downstream log-output consumer.
157 let raw: anyhow::Result<()> = Err(sample_err());
158 let via_trait = raw.flatten_ctx("depends_on check").unwrap_err();
159 let pre_lift = anyhow::anyhow!("depends_on check: {}", sample_err());
160 assert_eq!(
161 format!("{via_trait}"),
162 format!("{pre_lift}"),
163 "flatten_ctx wrap must be Display-identical to pre-lift anyhow! chain"
164 );
165 }
166
167 #[test]
168 fn flatten_ctx_ok_arm_is_a_pure_passthrough() {
169 // Ok-arm invariant: `flatten_ctx` on `Ok(t)` MUST return
170 // `Ok(t)` verbatim — no side-effect on the payload, no
171 // synthesis of a context-tagged error, no allocation. Peer to
172 // the Err-arm byte-shape pin; a regression that promoted the
173 // Ok arm to ALWAYS produce a synthesis Error would silently
174 // break every successful downstream primitive call in the
175 // pre-lift consumer set.
176 let raw: anyhow::Result<i32> = Ok(42);
177 assert_eq!(raw.flatten_ctx("noop").unwrap(), 42);
178 }
179
180 #[test]
181 fn flatten_ctx_with_owned_string_matches_pre_lift_format_bytewise() {
182 // Owned-string peer's byte-shape pin — same discipline as the
183 // static-`&str` peer above. Consumers that compose the context
184 // slug via `format!` (e.g. `format!("evaluate {:?}", c.kind)`)
185 // route through this method and inherit the SAME display-prefix
186 // discipline as the static-slug peer, so mixing the two forms
187 // across the reconciler's log stream never surfaces as a
188 // format-string skew.
189 let raw: anyhow::Result<()> = Err(sample_err());
190 let dynamic_slug = format!("evaluate {:?}", "HelmReleaseReleased");
191 let via_trait = raw.flatten_ctx_with(dynamic_slug.clone()).unwrap_err();
192 let pre_lift = anyhow::anyhow!("{}: {}", dynamic_slug, sample_err());
193 assert_eq!(
194 format!("{via_trait}"),
195 format!("{pre_lift}"),
196 "flatten_ctx_with wrap must be Display-identical to pre-lift anyhow! chain"
197 );
198 }
199
200 #[test]
201 fn flatten_ctx_with_ok_arm_is_a_pure_passthrough() {
202 // Ok-arm invariant on the owned-string peer — sibling to the
203 // static-slug pin above. A regression that promoted the Ok arm
204 // of the dynamic-slug peer to a synthesis while leaving the
205 // static-slug peer's Ok arm passthrough would surface HERE as
206 // an owned-string-peer-specific asymmetry rather than as silent
207 // drift at the one `phase_machine::evaluate_conditions` consumer.
208 let raw: anyhow::Result<&'static str> = Ok("condition satisfied");
209 assert_eq!(
210 raw.flatten_ctx_with("dynamic".to_string()).unwrap(),
211 "condition satisfied"
212 );
213 }
214
215 #[test]
216 fn flatten_ctx_static_and_owned_peers_produce_identical_output_for_the_same_slug() {
217 // Cross-peer coherence pin: given the SAME context slug via
218 // both peers (a `&'static str` passed to `flatten_ctx` and the
219 // owned `String` produced by `.to_string()` passed to
220 // `flatten_ctx_with`), the wrapped `anyhow::Error` MUST have
221 // byte-identical `Display` output. A regression that drifted
222 // one peer's format string away from the other would surface
223 // HERE rather than as silent operator-facing skew between the
224 // four static-slug consumers and the one `format!`-slug
225 // consumer in the same log stream.
226 let slug = "render routing";
227 let a: anyhow::Result<()> = Err(sample_err());
228 let b: anyhow::Result<()> = Err(sample_err());
229 assert_eq!(
230 format!("{}", a.flatten_ctx(slug).unwrap_err()),
231 format!("{}", b.flatten_ctx_with(slug.to_string()).unwrap_err()),
232 "static-str and owned-string peers must produce identical Display output"
233 );
234 }
235
236 #[test]
237 fn flatten_ctx_threads_the_underlying_anyhow_display_verbatim() {
238 // Display-tail invariant: the wrapped `anyhow::Error`'s
239 // `Display` output MUST contain the source `anyhow::Error`'s
240 // own `Display` output verbatim as the tail past `"<ctx>: "`.
241 // A regression that inserted a normalization (uppercase, JSON
242 // encoding, truncation) between the composed `{e}` slot and
243 // the underlying `Display` impl would surface HERE rather
244 // than as silent underlying-error-detail loss across the
245 // reconciler's error stream.
246 let underlying_display = format!("{}", sample_err());
247 let raw: anyhow::Result<()> = Err(sample_err());
248 let wrapped = raw.flatten_ctx("apply export job").unwrap_err();
249 let wrapped_display = format!("{wrapped}");
250 assert!(
251 wrapped_display.ends_with(&underlying_display),
252 "wrapped Display `{wrapped_display}` must end with underlying anyhow Display `{underlying_display}`"
253 );
254 assert!(
255 wrapped_display.starts_with("apply export job: "),
256 "wrapped Display `{wrapped_display}` must start with `\"<ctx>: \"`"
257 );
258 }
259}