Skip to main content

tatara_process/
json_object.rs

1//! Substrate primitive over `serde_json::Value` — the ONE substrate
2//! owner of the `.as_object_mut().ok_or_else(|| anyhow::anyhow!(
3//! "<slot> is not an object"))` guard-shape every JSON-mutating helper
4//! restates by hand at the "walk this `Value` slot into its
5//! `serde_json::Map` interior or fail loud" boundary.
6//!
7//! Peer of the trait family that already lives in this crate on the
8//! wrap-shape axis:
9//!
10//! * [`crate::kube_error::KubeResultExt`] — the `kube::Error → anyhow`
11//!   display-prefix wrap.
12//! * [`crate::hostname::HostnameResultExt`] — the `HostnameError →
13//!   anyhow` display-prefix wrap.
14//! * [`crate::anyhow_flatten::FlattenCtxExt`] — the `anyhow::Error →
15//!   anyhow` display-prefix flatten.
16//! * This module — the `Option<&mut Map> → anyhow::Result<&mut Map>`
17//!   type-guard, partitioned from the three above by SOURCE (`None`
18//!   from the slot-typecheck, not a lifted error type) but sharing the
19//!   `.map_err(|_| anyhow!("<slug>: …"))?` display-prefix wire format.
20//!
21//! Pre-lift the shape was hand-authored at THREE adjacent private
22//! helpers in `tatara-reconciler::ssapply` past the ★★ PRIME-DIRECTIVE
23//! ≥ 2 duplication threshold:
24//!
25//! * `metadata_object_mut(resource)` — the root-guard step
26//!   (`resource.as_object_mut().ok_or_else(|| anyhow!("resource is not
27//!   an object"))?`) that opens the SSA-time
28//!   `resource → &mut metadata` walk shared by `inject_owner_reference`
29//!   + `inject_annotations`.
30//! * `metadata_object_mut(resource)` — the metadata-slot type-check
31//!   step (`metadata.as_object_mut().ok_or_else(|| anyhow!("metadata
32//!   is not an object"))?`) that closes the same walk — a resource
33//!   whose author mistyped the `metadata` slot as an array / string
34//!   surfaces as an error rather than as a silent
35//!   `.as_object_mut() → None → skip` no-op.
36//! * `inject_annotations(resource, process)` — the annotations-slot
37//!   type-check step (`annot.as_object_mut().ok_or_else(|| anyhow!(
38//!   "annotations is not an object"))?`) that opens the SSA-time
39//!   `metadata → &mut annotations` walk before the ownership tag +
40//!   observed-* primitive family drops its keys into the map.
41//!
42//! All three restated the SAME 2-line shape verbatim: `.as_object_mut()`
43//! on a `serde_json::Value` handle already known to be non-null, then
44//! `.ok_or_else(|| anyhow!("<slot-name> is not an object"))` wrap
45//! whose slot name matched the walk step's semantic role (`"resource"`
46//! / `"metadata"` / `"annotations"`). THREE byte-for-byte identical
47//! guard blocks past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold,
48//! differing only in the `&'static str` slot name each callsite
49//! stamped.
50//!
51//! Post-lift each callsite reads
52//! `<value>.as_object_mut_or("<slot>")?` and the guard-shape lives at
53//! ONE substrate owner here. The composed `anyhow::Error`'s `Display`
54//! is byte-identical to the pre-lift chain (`"<slot> is not an
55//! object"`), so operator-facing log output and any error-chain greps
56//! still match bytewise. A regression that drifts the message (a
57//! `"<slot> is not a JSON object"` synonym, a swapped `<slot>` slot,
58//! a promotion to a chain-form `source` that only surfaces via the
59//! alternate `{e:#}` formatter) surfaces at the tests below rather
60//! than as silent operator-facing drift across the three pre-lift
61//! consumers.
62//!
63//! ### Naming — `as_object_mut_or`, not `as_object_mut`
64//!
65//! Same discipline as the three sibling traits above — the trait
66//! method deliberately does NOT share a name with the inherent
67//! `serde_json::Value::as_object_mut` method (which returns
68//! `Option<&mut Map>`), because a name collision would let a caller
69//! who has `ValueObjectExt` in scope resolve to the inherent method
70//! by accident (inherent methods win over trait methods in method
71//! resolution) and silently drop the type-guard wrap altogether. The
72//! `_or` suffix names the intent: guard the `Option → Result` step
73//! at the same call, matching the pre-lift `.as_object_mut().
74//! ok_or_else(...)` chain.
75//!
76//! ### `#[must_use]`
77//!
78//! Every consumer threads the `?` short-circuit onto its handler's
79//! `Result<_, anyhow::Error>` return — dropping the guard swallows
80//! the underlying type-mismatch entirely, which is never the intended
81//! semantic at any of the three pre-lift consumers (each downstream
82//! `md.entry(...).or_insert_with(...)` / `annot.insert(...)` mutation
83//! depends on the returned `&mut Map` reference).
84//!
85//! Theory anchor: THEORY.md §VI.1 (generation over composition — the
86//! `.as_object_mut().ok_or_else(|| anyhow!("<slot> is not an
87//! object"))` guard-shape recurred at three hand-authored sites past
88//! the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
89//! ONE substrate owner here). THEORY.md §II.1 invariant 5 (composition
90//! preserves proofs — a regression that drifts the guard message
91//! wording at ONE site surfaces here at the substrate pin rather than
92//! as silent operator-facing skew across every SSA-time
93//! `metadata_object_mut` + `inject_annotations` mutation).
94
95use serde_json::{Map, Value};
96
97/// Substrate extension trait over `serde_json::Value` — the ONE
98/// substrate owner of the `.as_object_mut().ok_or_else(|| anyhow!(
99/// "<slot> is not an object"))` guard-shape. See the module docs for
100/// the full callsite audit + the naming rationale (why
101/// `as_object_mut_or` and not `as_object_mut`).
102pub trait ValueObjectExt {
103    /// Borrow the [`Value`] as a mutable JSON object [`Map`], or fail
104    /// loud with an [`anyhow::Error`] whose `Display` reads exactly
105    /// `"<slot> is not an object"` — the pre-lift wire format every
106    /// consumer's `tracing::error!(error = %e, ...)` log line already
107    /// encoded.
108    #[must_use = "an object-guard that isn't threaded via `?` swallows the underlying type mismatch"]
109    fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>>;
110}
111
112impl ValueObjectExt for Value {
113    #[inline]
114    fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>> {
115        self.as_object_mut()
116            .ok_or_else(|| anyhow::anyhow!("{slot} is not an object"))
117    }
118}
119
120/// Substrate extension trait over `serde_json::Map<String, Value>` —
121/// the ONE substrate owner of the `map.insert(<key>.into(),
122/// Value::String(<val>.into()))` string-slot insertion shape every
123/// JSON-mutating helper in the workspace hand-authored at each callsite.
124///
125/// Peer of [`ValueObjectExt`] above on the JSON-mutation axis, split
126/// by SHAPE: [`ValueObjectExt::as_object_mut_or`] owns the "walk into
127/// this `Value`'s object-shape interior or fail loud" guard;
128/// [`JsonMapStrExt::insert_str`] owns the "stamp a `Value::String` at
129/// a string-typed key" write shape that every consumer downstream of
130/// the guard uses to populate the returned `&mut Map`.
131///
132/// Pre-lift the shape was hand-authored at THIRTEEN production emit
133/// sites across `tatara-reconciler` past the ★★ PRIME-DIRECTIVE ≥ 2
134/// duplication threshold:
135///
136/// * `ssapply::inject_annotations` × 4 — the SSA-time observed-*
137///   annotation stamp family (`PID`, `CONTENT_HASH`, `GENERATION`,
138///   `ATTESTATION_ROOT`) each restated the 2-line `annot.insert(
139///   <annotation-const>.to_string(), Value::String(<val>.<coerce>))`
140///   shape verbatim.
141/// * `render::render_flux` × 3 — the Flux `Kustomization.spec` seeds
142///   (`interval`, `path`, `targetNamespace`), each restating the same
143///   `spec.insert("<key>".into(), Value::String(<val>))` shape.
144/// * `render::render_aplicacao` × 3 — the Flux `HelmRelease.spec`
145///   seeds (`releaseName`, `targetNamespace`) plus the values-overlay
146///   `profile` slot, each restating the same insert shape.
147/// * `render::render_export_job` × 2 — the export-Job outer label map
148///   (`ROLE`, `EXPORT_INDEX`) each restating the same insert shape.
149/// * `edges::IngressEdge::render` × 1 — the cert-manager
150///   `cluster-issuer` annotation, restating the same insert shape.
151///
152/// All THIRTEEN pre-lift sites restated the SAME 2-line shape verbatim,
153/// differing only in the `&'static str` / `String` key + the `&str` /
154/// `String` value at each callsite. A copy-paste that dropped the
155/// `Value::String(...)` wrap (a caller who reached for
156/// `.insert(k, v)` after refactoring from a `Value` slot to a plain
157/// `String` value slot) would type-check silently at every callsite —
158/// `Map<String, Value>::insert` expects a `Value`, and `String:
159/// Into<Value>` is provided by `serde_json` via the `Value::String`
160/// arm's `From` impl, so the naive `.insert(k, v.to_string())` compiles
161/// AND writes the byte-identical JSON. Post-lift each callsite reads
162/// `<map>.insert_str(<key>, <val>)` and the string-slot write shape
163/// lives at ONE substrate owner here.
164///
165/// ### Composability
166///
167/// * Key slot accepts any `impl Into<String>`: `&str` (via
168///   `String::from`), `String` (identity), `Cow<'_, str>`, so a
169///   callsite with a static `annotations::PID` (`&'static str`) reads
170///   `insert_str(annotations::PID, …)` with no `.to_string()` per site.
171/// * Value slot accepts any `impl Into<String>`: `&str`, `String`,
172///   `Cow<'_, str>`. Numeric or non-string values still need an
173///   explicit `.to_string()` at the callsite — same as pre-lift, so
174///   the wrapping shape stays visible in the caller's grep footprint.
175/// * Returns `Option<Value>` matching the inherent
176///   `Map<String, Value>::insert` return semantics: `None` on new-key,
177///   `Some(prev)` on overwrite of an existing slot.
178///
179/// ### Naming — `insert_str`, not `insert`
180///
181/// Same discipline as [`ValueObjectExt::as_object_mut_or`] above — the
182/// trait method deliberately does NOT collide with the inherent
183/// `Map::insert` (which takes `(String, Value)` positionally). A name
184/// collision would let a caller who has `JsonMapStrExt` in scope
185/// resolve to the inherent method by accident (inherent methods win
186/// over trait methods in method resolution) and silently drop the
187/// `Value::String` wrap, stamping the value bytes straight into the
188/// map under a different `Value` variant. The `_str` suffix names the
189/// intent: the value slot IS the `Value::String` arm at this write.
190///
191/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
192/// `.insert(<k>.into(), Value::String(<v>.into()))` shape recurred at
193/// THIRTEEN hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
194/// duplication trigger, and is lifted to ONE substrate owner here).
195/// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
196/// regression that drifts the string-slot write shape at ONE consumer
197/// surfaces at the substrate pin rather than as silent per-emit skew
198/// across every ssapply / render / edges JSON emit site).
199pub trait JsonMapStrExt {
200    /// Insert a `Value::String(<val>.into())` at `<key>.into()` into
201    /// this JSON object map. Returns `Option<Value>` matching the
202    /// underlying `Map::insert` semantics — `None` for a new key,
203    /// `Some(prev)` for an overwrite.
204    fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value>;
205}
206
207impl JsonMapStrExt for Map<String, Value> {
208    #[inline]
209    fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value> {
210        self.insert(key.into(), Value::String(value.into()))
211    }
212}
213
214/// Substrate extension trait over `serde_json::Map<String, Value>` —
215/// the ONE substrate owner of the `.entry(<key>).or_insert_with(||
216/// Value::Object(<empty>))` seed-then-guard shape every JSON-mutating
217/// helper hand-authored at the "walk into this object slot on the
218/// parent map, seeding an empty object if the slot is absent, or fail
219/// loud if the slot exists but is a non-object" boundary.
220///
221/// Peer of [`ValueObjectExt::as_object_mut_or`] and
222/// [`JsonMapStrExt::insert_str`] on the JSON-mutation axis; split by
223/// SHAPE + SITE. [`ValueObjectExt::as_object_mut_or`] owns the "guard
224/// a `Value` handle into its object interior" step at ONE level;
225/// [`JsonMapStrExt::insert_str`] owns the "stamp a `Value::String` at
226/// a string-typed key" write shape; this trait owns the compound
227/// "get-or-seed the object at a slot, then guard" step every SSA-time
228/// re-injection walks when the caller intends to reach a nested
229/// object slot without asserting whether the parent has already
230/// populated it (a caller composing a fresh resource-body carries
231/// no `metadata` / `metadata.annotations` slot pre-seed; a caller
232/// composing atop a pre-populated resource does — both paths reach
233/// the same primitive).
234///
235/// Pre-lift the compound shape was hand-authored at TWO adjacent
236/// private helpers in `tatara-reconciler::ssapply` past the ★★
237/// PRIME-DIRECTIVE ≥ 2 duplication threshold, both walking the SAME
238/// 3-step `let X = <map>.entry(<slot>).or_insert_with(|| Value::Object
239/// (<empty>)); X.as_object_mut_or(<slot>)?` incantation:
240///
241/// * `metadata_object_mut(resource)` — the `metadata` slot seed-then-
242///   guard step at the root of every SSA-time re-injection walk
243///   (`inject_owner_reference` + `inject_annotations` reach it).
244/// * `inject_annotations(resource, process)` — the `annotations`
245///   slot seed-then-guard step nested one level deeper under the
246///   `metadata` object the primitive above returned.
247///
248/// Both restated the SAME 3-line shape verbatim: `.entry(<slot>)` on
249/// a `Map<String, Value>` handle known to be an object, then
250/// `.or_insert_with(|| Value::Object(<empty>))` to synthesize an
251/// empty object at the slot when absent, then a `.as_object_mut_or
252/// (<slot>)?` guard on the returned `&mut Value` to fail loud when
253/// the existing slot is a non-object. TWO byte-for-byte identical
254/// blocks past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold,
255/// differing only in the `&'static str` slot name each callsite
256/// stamped (`"metadata"` / `"annotations"`) — and the slot name is
257/// used at BOTH the entry key AND the guard error message so a
258/// regression that drifted the two apart at one callsite (a typo
259/// stamping `"metadata"` into the entry key + `"metadatas"` into
260/// the error message) would silently pass one pin and fail the
261/// other. Post-lift each callsite reads `<map>.object_slot_mut_or
262/// (<slot>)?` and the compound shape lives at ONE substrate owner
263/// here — the slot name is stamped ONCE per call and reaches both
264/// the entry key and the guard error slot mechanically.
265///
266/// ### Composability
267///
268/// * Slot name is `&'static str` — pre-lift both callsites stamped
269///   `&'static str` literals (`"metadata"` / `"annotations"`); a
270///   dynamic-slot caller (a callsite that reached this primitive
271///   with a `String` key computed at runtime) has no pre-lift
272///   precedent in the ssapply/render axis, so the `&'static str`
273///   bound stays honest to the pre-lift shape. A future caller
274///   needing a runtime slot name can widen this to
275///   `impl Into<String>` at the substrate; the pre-lift consumers
276///   inherit it mechanically.
277/// * Returns `anyhow::Result<&mut Map<String, Value>>` — matches the
278///   sibling [`ValueObjectExt::as_object_mut_or`] shape so the
279///   downstream `.entry(...).or_insert_with(...)` / `.insert(...)`
280///   mutation threads through `?` onto the caller's
281///   `Result<_, anyhow::Error>` return exactly as pre-lift.
282/// * Ok-arm returns the SAME `&mut Map<String, Value>` the pre-lift
283///   `.as_object_mut_or(<slot>)` step returned — no clone, no key-
284///   order reshape, no synthesis.
285///
286/// ### Naming — `object_slot_mut_or`, not `entry_object` or
287/// `get_or_insert_object_mut`
288///
289/// Same discipline as the two sibling traits above — the trait method
290/// deliberately does NOT collide with the inherent `Map::entry` /
291/// `Map::get_mut` / `Map::insert` methods (any of which a caller who
292/// has this trait in scope could resolve to by accident, silently
293/// dropping the type-guard step). The `_or` suffix names the intent
294/// (guard the `Option → Result` step at the same call, matching the
295/// pre-lift `.as_object_mut_or(<slot>)?` guard); `object_slot_mut`
296/// names the target shape (return an `&mut` object-typed `Map` at
297/// the slot). Together they read as "guard the slot into a mutable
298/// object interior or fail loud", matching the pre-lift semantics
299/// exactly.
300///
301/// ### `#[must_use]`
302///
303/// Every consumer threads the `?` short-circuit onto its handler's
304/// `Result<_, anyhow::Error>` return — dropping the guard swallows
305/// the underlying type-mismatch entirely, which is never the intended
306/// semantic at either pre-lift consumer (each downstream
307/// `.entry(...).or_insert_with(...)` / `.insert(...)` mutation
308/// depends on the returned `&mut Map` reference).
309///
310/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
311/// 3-line `.entry(<slot>).or_insert_with(|| Value::Object(<empty>))
312/// .as_object_mut_or(<slot>)?` compound shape recurred at two
313/// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
314/// trigger, and is lifted to ONE substrate owner here). THEORY.md
315/// §II.1 invariant 5 (composition preserves proofs — a regression
316/// that drifted the entry-key slot vs. the guard-error slot at ONE
317/// site would silently pass one downstream pin and fail the other;
318/// post-lift the primitive stamps the slot ONCE per call so the
319/// substrate itself owns the entry-key ↔ guard-error name coherence).
320pub trait JsonMapObjectEntryExt {
321    /// Get-or-seed the object at `slot` in this JSON map, then guard
322    /// that the resulting handle is an object; returns
323    /// `&mut Map<String, Value>` on the object arm, and an
324    /// [`anyhow::Error`] whose `Display` reads
325    /// `"<slot> is not an object"` on the non-object arm (byte-
326    /// identical to the pre-lift `.as_object_mut_or(<slot>)?` guard,
327    /// sourced from the sibling [`ValueObjectExt::as_object_mut_or`]).
328    #[must_use = "an object-slot guard that isn't threaded via `?` swallows the underlying type mismatch"]
329    fn object_slot_mut_or(&mut self, slot: &'static str)
330        -> anyhow::Result<&mut Map<String, Value>>;
331}
332
333impl JsonMapObjectEntryExt for Map<String, Value> {
334    #[inline]
335    fn object_slot_mut_or(
336        &mut self,
337        slot: &'static str,
338    ) -> anyhow::Result<&mut Map<String, Value>> {
339        self.entry(slot)
340            .or_insert_with(|| Value::Object(Map::new()))
341            .as_object_mut_or(slot)
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use serde_json::json;
349
350    // ─── ValueObjectExt::as_object_mut_or substrate pins ─────────────
351    //
352    // Fail-before-pass-after granularity: the `ValueObjectExt::
353    // as_object_mut_or` trait method did not exist before this commit,
354    // so each test below fails to compile pre-lift. Post-lift they
355    // collectively pin the object-guard shape at ONE substrate owner —
356    // a regression that drifts the error message wording, swaps the
357    // `<slot>` slot, wraps the source in a chain-form `source` (which
358    // would change `Display` output when downstream tracing formatters
359    // interpolate `{e}` rather than the chain-walking `{e:#}`), or
360    // promotes the pass-through arm to synthesis (a `None → Ok(&mut
361    // Map::default())` fallthrough that silently swallows a mistyped
362    // slot) surfaces HERE rather than as silent operator-facing skew
363    // across the three `ssapply.rs` pre-lift consumers whose log
364    // output already encoded the flat `"<slot> is not an object"`
365    // shape.
366
367    #[test]
368    fn as_object_mut_or_object_arm_returns_the_inner_map_mutably() {
369        // Ok-arm invariant: a `Value::Object` handle threaded through
370        // `as_object_mut_or("<slot>")` MUST return `Ok(&mut Map)`
371        // whose interior is the SAME `serde_json::Map` the underlying
372        // `serde_json::Value::as_object_mut` would return — no clone,
373        // no reshape, no synthesis. The `&mut` return is load-bearing
374        // at every consumer (each threads a downstream `.entry(...).
375        // or_insert_with(...)` / `.insert(...)` mutation onto the
376        // returned reference), so a regression that returned a fresh
377        // owned `Map` here would silently drop every downstream write.
378        let mut v = json!({ "existing_key": "existing_value" });
379        let map = v.as_object_mut_or("resource").expect("Value::Object");
380        map.insert("new_key".to_string(), json!("new_value"));
381        assert_eq!(v["existing_key"], "existing_value");
382        assert_eq!(v["new_key"], "new_value");
383    }
384
385    #[test]
386    fn as_object_mut_or_null_arm_errors_with_pre_lift_display_bytewise() {
387        // Byte-shape parity pin: the wrap output of `as_object_mut_or
388        // ("<slot>")` on a `Value::Null` handle MUST be `Display`-
389        // identical to the pre-lift hand-authored `.as_object_mut().
390        // ok_or_else(|| anyhow!("<slot> is not an object"))?` chain.
391        // A regression that inserted a synonym (`"<slot> is not a
392        // JSON object"`), reshaped the slot position (`"not an
393        // object: <slot>"`), or dropped the leading `<slot>` slot
394        // surfaces HERE rather than as silent drift at every
395        // downstream log-output consumer.
396        let mut v = Value::Null;
397        let err = v.as_object_mut_or("resource").unwrap_err();
398        assert_eq!(format!("{err}"), "resource is not an object");
399    }
400
401    #[test]
402    fn as_object_mut_or_array_arm_errors_with_pre_lift_display_bytewise() {
403        // Sibling to the null-arm byte-shape pin — a mistyped
404        // `metadata` slot authored as a JSON array (kubectl accepts
405        // `metadata: []` in a YAML manifest with no schema, though the
406        // apiserver later rejects it) surfaces the same guard error.
407        // Pins the "non-object variants ALL error via the same wire
408        // format" invariant — a regression that special-cased the
409        // array variant (returning a fresh empty map, silently
410        // coercing) surfaces HERE.
411        let mut v = json!(["not", "an", "object"]);
412        let err = v.as_object_mut_or("metadata").unwrap_err();
413        assert_eq!(format!("{err}"), "metadata is not an object");
414    }
415
416    #[test]
417    fn as_object_mut_or_string_arm_errors_with_pre_lift_display_bytewise() {
418        // Sibling to the null / array pins — a mistyped `annotations`
419        // slot authored as a JSON string (a common apiserver-layer
420        // authoring bug in kubectl-generated manifests where a
421        // stringified JSON object leaks through) surfaces the same
422        // guard error. Pins the "every non-object variant errors via
423        // the same wire format" invariant across the full
424        // `serde_json::Value` sum.
425        let mut v = json!("stringified");
426        let err = v.as_object_mut_or("annotations").unwrap_err();
427        assert_eq!(format!("{err}"), "annotations is not an object");
428    }
429
430    #[test]
431    fn as_object_mut_or_threads_the_slot_slug_verbatim_across_all_three_pre_lift_labels() {
432        // Cross-slot coherence pin: the three pre-lift consumers in
433        // `tatara-reconciler::ssapply` stamped THREE distinct slot
434        // slugs (`"resource"` / `"metadata"` / `"annotations"`), and
435        // the wrap-shape MUST honor each one verbatim as the leading
436        // slot in the `Display` output. A regression that hard-coded
437        // one slug (say `"resource"`) across every callsite would
438        // pass the first pin above and fail HERE — the three
439        // downstream error-stream greps operators run to bisect a
440        // "which SSA-time mutation faulted" alert would ALL collapse
441        // to the same slug.
442        for slot in ["resource", "metadata", "annotations"] {
443            let mut v = Value::Null;
444            let err = v.as_object_mut_or(slot).unwrap_err();
445            assert_eq!(format!("{err}"), format!("{slot} is not an object"));
446        }
447    }
448
449    #[test]
450    fn as_object_mut_or_object_arm_matches_inherent_as_object_mut_bytewise() {
451        // Cross-substrate coherence pin: on the Ok arm the trait
452        // method MUST return the SAME `&mut Map` the inherent
453        // `serde_json::Value::as_object_mut` would — no diverging
454        // view, no clone, no key-order reshape. A regression that
455        // introduced a normalization pass here (sorting keys,
456        // stripping a null-valued entry, coercing a nested string
457        // to a JSON scalar) would surface as silent per-consumer
458        // schema drift at the SSA-time mutation — an ownerReferences
459        // append that no longer landed in the same slot the apiserver
460        // reads, an annotations insert whose key ordering diverged
461        // from kubectl's canonical form.
462        let mut via_trait = json!({ "key": "value", "nested": { "inner": 1 } });
463        let mut via_inherent = via_trait.clone();
464        assert_eq!(
465            via_trait
466                .as_object_mut_or("resource")
467                .expect("Value::Object")
468                .clone(),
469            via_inherent.as_object_mut().expect("Value::Object").clone(),
470        );
471    }
472
473    // ─── JsonMapStrExt::insert_str substrate pins ─────────────────
474    //
475    // Fail-before-pass-after granularity: the `JsonMapStrExt::insert_str`
476    // trait method did not exist before this commit, so each test below
477    // fails to compile pre-lift. Post-lift they collectively pin the
478    // string-slot write shape at ONE substrate owner — a regression that
479    // dropped the `Value::String` wrap (silently coercing to a bare
480    // `Value::from(&str)` — byte-identical in the `Object` arm today but
481    // divergent for any future non-`&str` numeric caller who reached for
482    // `insert_str(k, n.to_string())`), swapped the key + value slot
483    // orientation, or drifted the return semantics from the inherent
484    // `Map::insert` (which returns the previous value on overwrite —
485    // load-bearing at any future caller that inspects the return) would
486    // surface HERE rather than as silent per-emit skew across the
487    // thirteen pre-lift `ssapply` + `render` + `edges` consumers.
488
489    #[test]
490    fn insert_str_new_key_returns_none_and_stamps_value_string() {
491        // New-key arm: matches inherent `Map::insert` return
492        // semantics — `None` for a fresh key — and stamps a
493        // `Value::String` (NOT `Value::from(&str)`, though they're
494        // byte-identical today) at the slot.
495        let mut m = Map::new();
496        let prev = m.insert_str("key", "value");
497        assert!(prev.is_none(), "new key returns None");
498        assert_eq!(m.get("key"), Some(&Value::String("value".to_string())));
499        assert!(matches!(m.get("key"), Some(Value::String(_))));
500    }
501
502    #[test]
503    fn insert_str_overwrite_returns_prior_value_and_stamps_new() {
504        // Overwrite arm: matches inherent `Map::insert` return
505        // semantics — `Some(prev)` on overwrite. Load-bearing for
506        // any future consumer that inspects the return to detect a
507        // slot collision (a fleet-wide sweep that flagged a
508        // duplicate SSA-time annotation stamp, for example).
509        let mut m = Map::new();
510        m.insert_str("key", "old");
511        let prev = m.insert_str("key", "new");
512        assert_eq!(prev, Some(Value::String("old".to_string())));
513        assert_eq!(m.get("key"), Some(&Value::String("new".to_string())));
514    }
515
516    #[test]
517    fn insert_str_accepts_str_and_owned_string_at_both_slots() {
518        // Composability pin: both slots MUST accept `&str` and
519        // `String` interchangeably — the pre-lift callsite inventory
520        // mixes both (SSA-time `annotations::PID` static + a
521        // `pid.to_string()` runtime String at the value slot;
522        // `spec.insert("interval".into(), Value::String("1m".into()))`
523        // with two `&str` slots). A regression that constrained
524        // either slot to one shape would break the callsite parity
525        // that motivated this substrate primitive.
526        let mut m1 = Map::new();
527        m1.insert_str("a", "b");
528        let mut m2 = Map::new();
529        m2.insert_str(String::from("a"), String::from("b"));
530        let mut m3 = Map::new();
531        m3.insert_str("a", String::from("b"));
532        let mut m4 = Map::new();
533        m4.insert_str(String::from("a"), "b");
534        assert_eq!(m1, m2);
535        assert_eq!(m2, m3);
536        assert_eq!(m3, m4);
537    }
538
539    #[test]
540    fn insert_str_matches_pre_lift_hand_authored_shape_bytewise() {
541        // Byte-shape parity pin: `insert_str(k, v)` MUST emit the
542        // SAME `Map` entry the pre-lift hand-authored `.insert(
543        // <k>.into(), Value::String(<v>.into()))` chain produced.
544        // Sweeps the four (str × String) × (str × String) key/value
545        // shape quadrants so a regression at the primitive that
546        // broke the byte identity with the pre-lift shape at ONE
547        // quadrant surfaces here rather than as a subtle per-emit
548        // divergence at that quadrant.
549        for (k_str, v_str) in [("a", "b"), ("x", ""), ("", "y"), ("", "")] {
550            // (str, str) quadrant
551            let mut via_primitive = Map::new();
552            via_primitive.insert_str(k_str, v_str);
553            let mut via_pre_lift = Map::new();
554            via_pre_lift.insert(k_str.into(), Value::String(v_str.into()));
555            assert_eq!(via_primitive, via_pre_lift);
556
557            // (String, String) quadrant
558            let mut via_primitive = Map::new();
559            via_primitive.insert_str(String::from(k_str), String::from(v_str));
560            let mut via_pre_lift = Map::new();
561            via_pre_lift.insert(String::from(k_str), Value::String(String::from(v_str)));
562            assert_eq!(via_primitive, via_pre_lift);
563        }
564    }
565
566    #[test]
567    fn insert_str_empty_value_stamps_empty_string_not_null() {
568        // Semantic pin: an empty value slot MUST stamp
569        // `Value::String("")`, NEVER `Value::Null`. Load-bearing at
570        // any callsite that stamps a placeholder empty-string
571        // annotation (say a `content_hash` slot pre-derive) where a
572        // `Null` slot would fail-loud at the K8s apiserver's
573        // annotation-value type check.
574        let mut m = Map::new();
575        m.insert_str("empty", "");
576        assert_eq!(m.get("empty"), Some(&Value::String(String::new())));
577        assert!(!matches!(m.get("empty"), Some(Value::Null)));
578    }
579
580    // ─── JsonMapObjectEntryExt::object_slot_mut_or substrate pins ─────
581    //
582    // Fail-before-pass-after granularity: the
583    // `JsonMapObjectEntryExt::object_slot_mut_or` trait method did not
584    // exist before this commit, so each test below fails to compile
585    // pre-lift. Post-lift they collectively pin the compound
586    // seed-then-guard shape at ONE substrate owner — a regression that
587    // dropped the seed step (leaving an absent slot to fall through the
588    // guard as `None → Err`), skipped the guard step (silently returning
589    // an `&mut Value` when the existing slot is a non-object variant),
590    // drifted the entry-key slot vs. the guard-error slot (a copy-paste
591    // typo that stamped `"metadata"` into the entry and `"metadatas"`
592    // into the guard error message), or drifted the empty-seed shape
593    // (a `Value::Null` fallback where `Value::Object(Map::new())` is
594    // load-bearing at the downstream `.entry(...).or_insert_with(...)`
595    // / `.insert(...)` mutation) would surface HERE rather than as
596    // silent per-emit skew across the two pre-lift `ssapply.rs`
597    // consumers.
598
599    #[test]
600    fn object_slot_mut_or_absent_slot_seeds_empty_object_and_returns_it() {
601        // Absent-slot arm: the pre-lift `.entry(<slot>).or_insert_with
602        // (|| Value::Object(Default::default()))` step MUST seed the
603        // slot with an EMPTY `Value::Object` when the slot is not
604        // present in the parent map. The returned handle is the fresh
605        // empty map, MUTABLY, so a downstream `.insert(...)` writes
606        // land in the parent map's `<slot>` object post-return.
607        let mut parent = Map::new();
608        {
609            let child = parent
610                .object_slot_mut_or("metadata")
611                .expect("absent slot seeds an object");
612            assert!(child.is_empty(), "fresh-seeded slot is an empty object");
613            child.insert("name".into(), Value::String("demo".into()));
614        }
615        // The write landed in the parent map's metadata slot.
616        assert_eq!(parent["metadata"]["name"], "demo");
617        assert!(matches!(parent.get("metadata"), Some(Value::Object(_))));
618    }
619
620    #[test]
621    fn object_slot_mut_or_present_object_slot_returns_existing_interior_mutably() {
622        // Present-object-slot arm: when the slot is already populated
623        // with a `Value::Object`, the primitive MUST return the
624        // EXISTING map interior mutably — no synthesis, no reshape, no
625        // key-order rewrite. The downstream `.insert(...)` writes MUST
626        // merge into the pre-existing keys rather than replace them.
627        let mut parent = Map::new();
628        parent.insert(
629            "metadata".into(),
630            serde_json::json!({ "existing_key": "existing_value" }),
631        );
632        {
633            let child = parent
634                .object_slot_mut_or("metadata")
635                .expect("present-object slot returns Ok");
636            assert_eq!(
637                child.get("existing_key"),
638                Some(&Value::String("existing_value".into()))
639            );
640            child.insert("new_key".into(), Value::String("new_value".into()));
641        }
642        assert_eq!(parent["metadata"]["existing_key"], "existing_value");
643        assert_eq!(parent["metadata"]["new_key"], "new_value");
644    }
645
646    #[test]
647    fn object_slot_mut_or_present_non_object_slot_errors_with_pre_lift_display() {
648        // Fail-loud arm: when the slot is present but holds a non-
649        // object variant (a `Value::String` from a hand-authored
650        // YAML manifest where `metadata: "malformed"` slipped past
651        // kubectl's schema check), the primitive MUST fail with a
652        // `Display` byte-identical to the pre-lift
653        // `.as_object_mut_or(<slot>)?` guard — the sibling
654        // [`ValueObjectExt::as_object_mut_or`] guard's wire format.
655        // A regression that special-cased this arm (overwriting the
656        // slot with a fresh empty object, silently coercing) would
657        // silently swallow the operator's authoring error at the
658        // SSA-time re-injection step.
659        let mut parent = Map::new();
660        parent.insert("metadata".into(), Value::String("malformed".into()));
661        let err = parent.object_slot_mut_or("metadata").unwrap_err();
662        assert_eq!(format!("{err}"), "metadata is not an object");
663    }
664
665    #[test]
666    fn object_slot_mut_or_threads_the_slot_slug_verbatim_across_both_pre_lift_labels() {
667        // Cross-slot coherence pin: the TWO pre-lift consumers in
668        // `tatara-reconciler::ssapply` stamped TWO distinct slot slugs
669        // (`"metadata"` at the resource root, `"annotations"` at the
670        // metadata child), and the wrap-shape MUST honor each one
671        // verbatim as the leading slot in the `Display` output. A
672        // regression that hard-coded one slug across every callsite
673        // would pass the fail-loud pin above (on the `"metadata"` slug)
674        // and fail HERE — the two downstream error-stream greps
675        // operators run to bisect a "which SSA-time slot mutation
676        // faulted" alert would ALL collapse to the same slug, hiding
677        // whether the fault was at the resource-root object walk or
678        // the metadata-child annotations walk.
679        for slot in ["metadata", "annotations"] {
680            let mut parent = Map::new();
681            parent.insert(slot.into(), Value::Null);
682            let err = parent.object_slot_mut_or(slot).unwrap_err();
683            assert_eq!(format!("{err}"), format!("{slot} is not an object"));
684        }
685    }
686
687    #[test]
688    fn object_slot_mut_or_present_empty_object_returns_existing_reference_not_synthesized() {
689        // Precedence pin: a present slot holding an EMPTY
690        // `Value::Object` MUST return the pre-existing empty map
691        // interior — not a freshly-synthesized replacement. The
692        // pre-lift `.entry(<slot>).or_insert_with(||...)` step's
693        // short-circuit on the present-slot arm skips the closure
694        // entirely; a regression that always evaluated the closure
695        // (unconditionally overwriting an existing empty-object slot
696        // with a fresh empty object) would type-check silently at
697        // every callsite AND write byte-identical JSON at the empty-
698        // slot corner, but it would break a hypothetical future
699        // consumer that reached the primitive on a map whose slot
700        // was seeded upstream with metadata (a caller intending to
701        // preserve any keys the parent-composer already dropped in).
702        let mut parent = Map::new();
703        parent.insert("metadata".into(), Value::Object(Map::new()));
704        let addr_before = parent.get("metadata").unwrap() as *const Value;
705        {
706            let _child = parent.object_slot_mut_or("metadata").unwrap();
707        }
708        let addr_after = parent.get("metadata").unwrap() as *const Value;
709        assert_eq!(
710            addr_before, addr_after,
711            "present empty-object slot must return the pre-existing reference, not a fresh synthesis",
712        );
713    }
714
715    #[test]
716    fn object_slot_mut_or_matches_pre_lift_hand_authored_compound_shape_bytewise() {
717        // Byte-shape parity pin: `object_slot_mut_or(<slot>)?` MUST
718        // produce the SAME `&mut Map` (and, on the non-object arm, the
719        // SAME `Display`-shaped error) the pre-lift 3-line `.entry
720        // (<slot>).or_insert_with(|| Value::Object(Default::default()))
721        // .as_object_mut_or(<slot>)?` chain produced. Sweeps the three
722        // pre-lift-reachable input corners (absent slot / present
723        // object / present non-object) so a regression at the primitive
724        // that broke byte identity with the pre-lift chain at ONE
725        // corner surfaces here rather than as a subtle per-emit
726        // divergence.
727        for slot in ["metadata", "annotations"] {
728            // (1) Absent-slot corner: both routes seed empty-object at
729            //     the slot AND return the same empty map interior.
730            let mut via_primitive = Map::new();
731            let mut via_pre_lift = Map::new();
732            {
733                let _ = via_primitive.object_slot_mut_or(slot).unwrap();
734                let _ = via_pre_lift
735                    .entry(slot.to_string())
736                    .or_insert_with(|| Value::Object(Map::new()))
737                    .as_object_mut_or(slot)
738                    .unwrap();
739            }
740            assert_eq!(via_primitive, via_pre_lift);
741
742            // (2) Present-object corner: both routes read back the
743            //     same pre-populated interior mutably.
744            let mut via_primitive = Map::new();
745            via_primitive.insert(slot.into(), serde_json::json!({ "k": "v" }));
746            let mut via_pre_lift = via_primitive.clone();
747            {
748                let a = via_primitive.object_slot_mut_or(slot).unwrap();
749                let b = via_pre_lift
750                    .entry(slot.to_string())
751                    .or_insert_with(|| Value::Object(Map::new()))
752                    .as_object_mut_or(slot)
753                    .unwrap();
754                assert_eq!(a, b);
755            }
756
757            // (3) Present-non-object corner: both routes fail loud
758            //     with the same wire-format Display shape.
759            let mut via_primitive = Map::new();
760            via_primitive.insert(slot.into(), Value::Bool(true));
761            let mut via_pre_lift = via_primitive.clone();
762            let err_primitive = via_primitive.object_slot_mut_or(slot).unwrap_err();
763            let err_pre_lift = via_pre_lift
764                .entry(slot.to_string())
765                .or_insert_with(|| Value::Object(Map::new()))
766                .as_object_mut_or(slot)
767                .unwrap_err();
768            assert_eq!(format!("{err_primitive}"), format!("{err_pre_lift}"));
769        }
770    }
771}