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//!   The module also owns the READ-side [`ValueGetExt`] projector
21//!   (`.get_i64(<key>) -> Option<i64>`) — sibling of the three
22//!   MUTATION-side traits below on the (read, mutate) axis, closing
23//!   the READ half of the `serde_json::Value` substrate the four
24//!   traits jointly own.
25//!
26//! Pre-lift the shape was hand-authored at THREE adjacent private
27//! helpers in `tatara-reconciler::ssapply` past the ★★ PRIME-DIRECTIVE
28//! ≥ 2 duplication threshold:
29//!
30//! * `metadata_object_mut(resource)` — the root-guard step
31//!   (`resource.as_object_mut().ok_or_else(|| anyhow!("resource is not
32//!   an object"))?`) that opens the SSA-time
33//!   `resource → &mut metadata` walk shared by `inject_owner_reference`
34//!   + `inject_annotations`.
35//! * `metadata_object_mut(resource)` — the metadata-slot type-check
36//!   step (`metadata.as_object_mut().ok_or_else(|| anyhow!("metadata
37//!   is not an object"))?`) that closes the same walk — a resource
38//!   whose author mistyped the `metadata` slot as an array / string
39//!   surfaces as an error rather than as a silent
40//!   `.as_object_mut() → None → skip` no-op.
41//! * `inject_annotations(resource, process)` — the annotations-slot
42//!   type-check step (`annot.as_object_mut().ok_or_else(|| anyhow!(
43//!   "annotations is not an object"))?`) that opens the SSA-time
44//!   `metadata → &mut annotations` walk before the ownership tag +
45//!   observed-* primitive family drops its keys into the map.
46//!
47//! All three restated the SAME 2-line shape verbatim: `.as_object_mut()`
48//! on a `serde_json::Value` handle already known to be non-null, then
49//! `.ok_or_else(|| anyhow!("<slot-name> is not an object"))` wrap
50//! whose slot name matched the walk step's semantic role (`"resource"`
51//! / `"metadata"` / `"annotations"`). THREE byte-for-byte identical
52//! guard blocks past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold,
53//! differing only in the `&'static str` slot name each callsite
54//! stamped.
55//!
56//! Post-lift each callsite reads
57//! `<value>.as_object_mut_or("<slot>")?` and the guard-shape lives at
58//! ONE substrate owner here. The composed `anyhow::Error`'s `Display`
59//! is byte-identical to the pre-lift chain (`"<slot> is not an
60//! object"`), so operator-facing log output and any error-chain greps
61//! still match bytewise. A regression that drifts the message (a
62//! `"<slot> is not a JSON object"` synonym, a swapped `<slot>` slot,
63//! a promotion to a chain-form `source` that only surfaces via the
64//! alternate `{e:#}` formatter) surfaces at the tests below rather
65//! than as silent operator-facing drift across the three pre-lift
66//! consumers.
67//!
68//! ### Naming — `as_object_mut_or`, not `as_object_mut`
69//!
70//! Same discipline as the three sibling traits above — the trait
71//! method deliberately does NOT share a name with the inherent
72//! `serde_json::Value::as_object_mut` method (which returns
73//! `Option<&mut Map>`), because a name collision would let a caller
74//! who has `ValueObjectExt` in scope resolve to the inherent method
75//! by accident (inherent methods win over trait methods in method
76//! resolution) and silently drop the type-guard wrap altogether. The
77//! `_or` suffix names the intent: guard the `Option → Result` step
78//! at the same call, matching the pre-lift `.as_object_mut().
79//! ok_or_else(...)` chain.
80//!
81//! ### `#[must_use]`
82//!
83//! Every consumer threads the `?` short-circuit onto its handler's
84//! `Result<_, anyhow::Error>` return — dropping the guard swallows
85//! the underlying type-mismatch entirely, which is never the intended
86//! semantic at any of the three pre-lift consumers (each downstream
87//! `md.entry(...).or_insert_with(...)` / `annot.insert(...)` mutation
88//! depends on the returned `&mut Map` reference).
89//!
90//! Theory anchor: THEORY.md §VI.1 (generation over composition — the
91//! `.as_object_mut().ok_or_else(|| anyhow!("<slot> is not an
92//! object"))` guard-shape recurred at three hand-authored sites past
93//! the ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
94//! ONE substrate owner here). THEORY.md §II.1 invariant 5 (composition
95//! preserves proofs — a regression that drifts the guard message
96//! wording at ONE site surfaces here at the substrate pin rather than
97//! as silent operator-facing skew across every SSA-time
98//! `metadata_object_mut` + `inject_annotations` mutation).
99
100use serde_json::{Map, Value};
101
102/// Substrate extension trait over `serde_json::Value` — the ONE
103/// substrate owner of the `.as_object_mut().ok_or_else(|| anyhow!(
104/// "<slot> is not an object"))` guard-shape. See the module docs for
105/// the full callsite audit + the naming rationale (why
106/// `as_object_mut_or` and not `as_object_mut`).
107pub trait ValueObjectExt {
108    /// Borrow the [`Value`] as a mutable JSON object [`Map`], or fail
109    /// loud with an [`anyhow::Error`] whose `Display` reads exactly
110    /// `"<slot> is not an object"` — the pre-lift wire format every
111    /// consumer's `tracing::error!(error = %e, ...)` log line already
112    /// encoded.
113    #[must_use = "an object-guard that isn't threaded via `?` swallows the underlying type mismatch"]
114    fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>>;
115}
116
117impl ValueObjectExt for Value {
118    #[inline]
119    fn as_object_mut_or(&mut self, slot: &'static str) -> anyhow::Result<&mut Map<String, Value>> {
120        self.as_object_mut()
121            .ok_or_else(|| anyhow::anyhow!("{slot} is not an object"))
122    }
123}
124
125/// Substrate extension trait over `serde_json::Map<String, Value>` —
126/// the ONE substrate owner of the `map.insert(<key>.into(),
127/// Value::String(<val>.into()))` string-slot insertion shape every
128/// JSON-mutating helper in the workspace hand-authored at each callsite.
129///
130/// Peer of [`ValueObjectExt`] above on the JSON-mutation axis, split
131/// by SHAPE: [`ValueObjectExt::as_object_mut_or`] owns the "walk into
132/// this `Value`'s object-shape interior or fail loud" guard;
133/// [`JsonMapStrExt::insert_str`] owns the "stamp a `Value::String` at
134/// a string-typed key" write shape that every consumer downstream of
135/// the guard uses to populate the returned `&mut Map`.
136///
137/// Pre-lift the shape was hand-authored at THIRTEEN production emit
138/// sites across `tatara-reconciler` past the ★★ PRIME-DIRECTIVE ≥ 2
139/// duplication threshold:
140///
141/// * `ssapply::inject_annotations` × 4 — the SSA-time observed-*
142///   annotation stamp family (`PID`, `CONTENT_HASH`, `GENERATION`,
143///   `ATTESTATION_ROOT`) each restated the 2-line `annot.insert(
144///   <annotation-const>.to_string(), Value::String(<val>.<coerce>))`
145///   shape verbatim.
146/// * `render::render_flux` × 3 — the Flux `Kustomization.spec` seeds
147///   (`interval`, `path`, `targetNamespace`), each restating the same
148///   `spec.insert("<key>".into(), Value::String(<val>))` shape.
149/// * `render::render_aplicacao` × 3 — the Flux `HelmRelease.spec`
150///   seeds (`releaseName`, `targetNamespace`) plus the values-overlay
151///   `profile` slot, each restating the same insert shape.
152/// * `render::render_export_job` × 2 — the export-Job outer label map
153///   (`ROLE`, `EXPORT_INDEX`) each restating the same insert shape.
154/// * `edges::IngressEdge::render` × 1 — the cert-manager
155///   `cluster-issuer` annotation, restating the same insert shape.
156///
157/// All THIRTEEN pre-lift sites restated the SAME 2-line shape verbatim,
158/// differing only in the `&'static str` / `String` key + the `&str` /
159/// `String` value at each callsite. A copy-paste that dropped the
160/// `Value::String(...)` wrap (a caller who reached for
161/// `.insert(k, v)` after refactoring from a `Value` slot to a plain
162/// `String` value slot) would type-check silently at every callsite —
163/// `Map<String, Value>::insert` expects a `Value`, and `String:
164/// Into<Value>` is provided by `serde_json` via the `Value::String`
165/// arm's `From` impl, so the naive `.insert(k, v.to_string())` compiles
166/// AND writes the byte-identical JSON. Post-lift each callsite reads
167/// `<map>.insert_str(<key>, <val>)` and the string-slot write shape
168/// lives at ONE substrate owner here.
169///
170/// ### Composability
171///
172/// * Key slot accepts any `impl Into<String>`: `&str` (via
173///   `String::from`), `String` (identity), `Cow<'_, str>`, so a
174///   callsite with a static `annotations::PID` (`&'static str`) reads
175///   `insert_str(annotations::PID, …)` with no `.to_string()` per site.
176/// * Value slot accepts any `impl Into<String>`: `&str`, `String`,
177///   `Cow<'_, str>`. Numeric or non-string values still need an
178///   explicit `.to_string()` at the callsite — same as pre-lift, so
179///   the wrapping shape stays visible in the caller's grep footprint.
180/// * Returns `Option<Value>` matching the inherent
181///   `Map<String, Value>::insert` return semantics: `None` on new-key,
182///   `Some(prev)` on overwrite of an existing slot.
183///
184/// ### Naming — `insert_str`, not `insert`
185///
186/// Same discipline as [`ValueObjectExt::as_object_mut_or`] above — the
187/// trait method deliberately does NOT collide with the inherent
188/// `Map::insert` (which takes `(String, Value)` positionally). A name
189/// collision would let a caller who has `JsonMapStrExt` in scope
190/// resolve to the inherent method by accident (inherent methods win
191/// over trait methods in method resolution) and silently drop the
192/// `Value::String` wrap, stamping the value bytes straight into the
193/// map under a different `Value` variant. The `_str` suffix names the
194/// intent: the value slot IS the `Value::String` arm at this write.
195///
196/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
197/// `.insert(<k>.into(), Value::String(<v>.into()))` shape recurred at
198/// THIRTEEN hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
199/// duplication trigger, and is lifted to ONE substrate owner here).
200/// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
201/// regression that drifts the string-slot write shape at ONE consumer
202/// surfaces at the substrate pin rather than as silent per-emit skew
203/// across every ssapply / render / edges JSON emit site).
204pub trait JsonMapStrExt {
205    /// Insert a `Value::String(<val>.into())` at `<key>.into()` into
206    /// this JSON object map. Returns `Option<Value>` matching the
207    /// underlying `Map::insert` semantics — `None` for a new key,
208    /// `Some(prev)` for an overwrite.
209    fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value>;
210}
211
212impl JsonMapStrExt for Map<String, Value> {
213    #[inline]
214    fn insert_str(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<Value> {
215        self.insert(key.into(), Value::String(value.into()))
216    }
217}
218
219/// Substrate extension trait over `serde_json::Map<String, Value>` —
220/// the ONE substrate owner of the `.entry(<key>).or_insert_with(||
221/// Value::Object(<empty>))` seed-then-guard shape every JSON-mutating
222/// helper hand-authored at the "walk into this object slot on the
223/// parent map, seeding an empty object if the slot is absent, or fail
224/// loud if the slot exists but is a non-object" boundary.
225///
226/// Peer of [`ValueObjectExt::as_object_mut_or`] and
227/// [`JsonMapStrExt::insert_str`] on the JSON-mutation axis; split by
228/// SHAPE + SITE. [`ValueObjectExt::as_object_mut_or`] owns the "guard
229/// a `Value` handle into its object interior" step at ONE level;
230/// [`JsonMapStrExt::insert_str`] owns the "stamp a `Value::String` at
231/// a string-typed key" write shape; this trait owns the compound
232/// "get-or-seed the object at a slot, then guard" step every SSA-time
233/// re-injection walks when the caller intends to reach a nested
234/// object slot without asserting whether the parent has already
235/// populated it (a caller composing a fresh resource-body carries
236/// no `metadata` / `metadata.annotations` slot pre-seed; a caller
237/// composing atop a pre-populated resource does — both paths reach
238/// the same primitive).
239///
240/// Pre-lift the compound shape was hand-authored at TWO adjacent
241/// private helpers in `tatara-reconciler::ssapply` past the ★★
242/// PRIME-DIRECTIVE ≥ 2 duplication threshold, both walking the SAME
243/// 3-step `let X = <map>.entry(<slot>).or_insert_with(|| Value::Object
244/// (<empty>)); X.as_object_mut_or(<slot>)?` incantation:
245///
246/// * `metadata_object_mut(resource)` — the `metadata` slot seed-then-
247///   guard step at the root of every SSA-time re-injection walk
248///   (`inject_owner_reference` + `inject_annotations` reach it).
249/// * `inject_annotations(resource, process)` — the `annotations`
250///   slot seed-then-guard step nested one level deeper under the
251///   `metadata` object the primitive above returned.
252///
253/// Both restated the SAME 3-line shape verbatim: `.entry(<slot>)` on
254/// a `Map<String, Value>` handle known to be an object, then
255/// `.or_insert_with(|| Value::Object(<empty>))` to synthesize an
256/// empty object at the slot when absent, then a `.as_object_mut_or
257/// (<slot>)?` guard on the returned `&mut Value` to fail loud when
258/// the existing slot is a non-object. TWO byte-for-byte identical
259/// blocks past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold,
260/// differing only in the `&'static str` slot name each callsite
261/// stamped (`"metadata"` / `"annotations"`) — and the slot name is
262/// used at BOTH the entry key AND the guard error message so a
263/// regression that drifted the two apart at one callsite (a typo
264/// stamping `"metadata"` into the entry key + `"metadatas"` into
265/// the error message) would silently pass one pin and fail the
266/// other. Post-lift each callsite reads `<map>.object_slot_mut_or
267/// (<slot>)?` and the compound shape lives at ONE substrate owner
268/// here — the slot name is stamped ONCE per call and reaches both
269/// the entry key and the guard error slot mechanically.
270///
271/// ### Composability
272///
273/// * Slot name is `&'static str` — pre-lift both callsites stamped
274///   `&'static str` literals (`"metadata"` / `"annotations"`); a
275///   dynamic-slot caller (a callsite that reached this primitive
276///   with a `String` key computed at runtime) has no pre-lift
277///   precedent in the ssapply/render axis, so the `&'static str`
278///   bound stays honest to the pre-lift shape. A future caller
279///   needing a runtime slot name can widen this to
280///   `impl Into<String>` at the substrate; the pre-lift consumers
281///   inherit it mechanically.
282/// * Returns `anyhow::Result<&mut Map<String, Value>>` — matches the
283///   sibling [`ValueObjectExt::as_object_mut_or`] shape so the
284///   downstream `.entry(...).or_insert_with(...)` / `.insert(...)`
285///   mutation threads through `?` onto the caller's
286///   `Result<_, anyhow::Error>` return exactly as pre-lift.
287/// * Ok-arm returns the SAME `&mut Map<String, Value>` the pre-lift
288///   `.as_object_mut_or(<slot>)` step returned — no clone, no key-
289///   order reshape, no synthesis.
290///
291/// ### Naming — `object_slot_mut_or`, not `entry_object` or
292/// `get_or_insert_object_mut`
293///
294/// Same discipline as the two sibling traits above — the trait method
295/// deliberately does NOT collide with the inherent `Map::entry` /
296/// `Map::get_mut` / `Map::insert` methods (any of which a caller who
297/// has this trait in scope could resolve to by accident, silently
298/// dropping the type-guard step). The `_or` suffix names the intent
299/// (guard the `Option → Result` step at the same call, matching the
300/// pre-lift `.as_object_mut_or(<slot>)?` guard); `object_slot_mut`
301/// names the target shape (return an `&mut` object-typed `Map` at
302/// the slot). Together they read as "guard the slot into a mutable
303/// object interior or fail loud", matching the pre-lift semantics
304/// exactly.
305///
306/// ### `#[must_use]`
307///
308/// Every consumer threads the `?` short-circuit onto its handler's
309/// `Result<_, anyhow::Error>` return — dropping the guard swallows
310/// the underlying type-mismatch entirely, which is never the intended
311/// semantic at either pre-lift consumer (each downstream
312/// `.entry(...).or_insert_with(...)` / `.insert(...)` mutation
313/// depends on the returned `&mut Map` reference).
314///
315/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
316/// 3-line `.entry(<slot>).or_insert_with(|| Value::Object(<empty>))
317/// .as_object_mut_or(<slot>)?` compound shape recurred at two
318/// hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
319/// trigger, and is lifted to ONE substrate owner here). THEORY.md
320/// §II.1 invariant 5 (composition preserves proofs — a regression
321/// that drifted the entry-key slot vs. the guard-error slot at ONE
322/// site would silently pass one downstream pin and fail the other;
323/// post-lift the primitive stamps the slot ONCE per call so the
324/// substrate itself owns the entry-key ↔ guard-error name coherence).
325pub trait JsonMapObjectEntryExt {
326    /// Get-or-seed the object at `slot` in this JSON map, then guard
327    /// that the resulting handle is an object; returns
328    /// `&mut Map<String, Value>` on the object arm, and an
329    /// [`anyhow::Error`] whose `Display` reads
330    /// `"<slot> is not an object"` on the non-object arm (byte-
331    /// identical to the pre-lift `.as_object_mut_or(<slot>)?` guard,
332    /// sourced from the sibling [`ValueObjectExt::as_object_mut_or`]).
333    #[must_use = "an object-slot guard that isn't threaded via `?` swallows the underlying type mismatch"]
334    fn object_slot_mut_or(&mut self, slot: &'static str)
335        -> anyhow::Result<&mut Map<String, Value>>;
336}
337
338impl JsonMapObjectEntryExt for Map<String, Value> {
339    #[inline]
340    fn object_slot_mut_or(
341        &mut self,
342        slot: &'static str,
343    ) -> anyhow::Result<&mut Map<String, Value>> {
344        self.entry(slot)
345            .or_insert_with(|| Value::Object(Map::new()))
346            .as_object_mut_or(slot)
347    }
348}
349
350/// Substrate extension trait over `serde_json::Value` — the ONE
351/// substrate owner of the paired `.get(<key>).and_then(|v| v.as_<T>())`
352/// two-link READ chain every downstream projection walks to pull a
353/// typed leaf off a Kubernetes-status blob (or an equivalent
354/// rendered-resource JSON object) without asserting the slot is
355/// present, without asserting its variant, and without asserting the
356/// slot fits the target scalar type.
357///
358/// The trait carries ONE method per typed READ axis; the axis-family
359/// is [`Self::get_i64`] (integer counters) + [`Self::get_str`]
360/// (string slots) + [`Self::get_array`] (JSON array slots). Adding a
361/// new axis (a `get_bool` for `Value::Bool`, a `get_object` for
362/// `Value::Object`, a `get_f64` for `Value::Number` truncated to
363/// `f64`) lands as ONE new method here + ONE impl arm, inheriting
364/// the naming, `#[must_use]`, and inline discipline the existing
365/// axes pin. Never open a peer trait for a new axis — keep every
366/// READ projection on the ONE substrate owner so a caller who
367/// imports `ValueGetExt` reaches every axis through the same trait
368/// handle.
369///
370/// READ-side counterpart to the three MUTATION-side siblings already in
371/// this module — [`ValueObjectExt::as_object_mut_or`],
372/// [`JsonMapStrExt::insert_str`], [`JsonMapObjectEntryExt::object_slot_mut_or`]
373/// — partitioning the substrate along the (read, mutate) axis on the
374/// same `serde_json::Value` / `serde_json::Map<String, Value>` carrier
375/// pair.
376///
377/// Pre-lift the two-link chain was hand-authored at THREE adjacent
378/// slots inside `tatara-reconciler::boundary::fetch_job_status`, each
379/// projecting one `batch/v1::Job` `status.<counter>` field out of the
380/// fetched `serde_json::Value` object into a private `JobStatusView`
381/// row past the ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold:
382///
383/// * `status.get("succeeded").and_then(|v| v.as_i64())` — the
384///   Job-completion counter every `JobAttested` + `ClosedLoopAuth`
385///   postcondition evaluator gates on (`succeeded < 1` short-circuits
386///   to `Satisfaction::Unsatisfied("… still running (…)")`).
387/// * `status.get("failed").and_then(|v| v.as_i64())` — the
388///   Job-failure counter the same evaluators gate on
389///   (`failed > 0` short-circuits to
390///   `Satisfaction::Unsatisfied("… failed (status.failed={n})")`).
391/// * `status.get("active").and_then(|v| v.as_i64())` — the
392///   Job-in-flight counter the "still running" diagnostic tail
393///   reports as `(succeeded={s}, active={a})`.
394///
395/// All THREE sites walked the SAME two-link chain — `.get(<key>)` on a
396/// `serde_json::Value` already known to be the status object, then
397/// `.and_then(|v| v.as_i64())` on the returned `Option<&Value>` — and
398/// each was followed by an `if let Some(...)` write into the
399/// [`JobStatusView`] row initialised from `Default::default()`. Post-
400/// lift each callsite reads `status.get_i64(<key>)` and the two-link
401/// READ chain lives at ONE substrate owner here.
402///
403/// ### Naming — `get_i64`, not `as_i64` or `i64_at`
404///
405/// Same discipline as the three sibling traits above — the trait method
406/// deliberately does NOT collide with `serde_json::Value::as_i64` (the
407/// inherent projection on a single `Value` handle) nor with
408/// `serde_json::Value::get` (the inherent slot-lookup returning
409/// `Option<&Value>`). A name collision would let a caller who has
410/// `ValueGetExt` in scope resolve to one of the inherent methods by
411/// accident (inherent methods win over trait methods in method
412/// resolution) and silently drop half of the paired chain. The
413/// `get_i64(<key>)` shape names the intent: look up the slot at
414/// `<key>`, project the returned handle to `i64`, in ONE call.
415///
416/// ### `#[must_use]`
417///
418/// Every consumer either binds the returned `Option<i64>` into a
419/// downstream `if let Some(n) = ...` / `.unwrap_or_default()` / struct-
420/// field construction. Dropping the return silently discards the
421/// projection entirely, which is never the intended semantic at the
422/// three pre-lift consumers (each downstream write depends on the
423/// returned counter).
424///
425/// ### Composability
426///
427/// * Key slot is `&str` — matches every pre-lift `.get("<literal>")`
428///   callsite and the inherent `serde_json::Value::get`'s primary
429///   `str`-index arm. A caller with a runtime-computed key (a
430///   `String` produced by a template composer) reaches through
431///   `.get_i64(&s)` mechanically via `Deref<Target = str>`.
432/// * Returns `Option<i64>` matching the composed inherent chain's own
433///   return; a consumer wanting the "absent or non-integer → 0"
434///   fallback composes `.unwrap_or_default()` (or `.unwrap_or(0)`) at
435///   the callsite, keeping the "should this counter default to 0 or
436///   fail loud" decision at the caller rather than baking it into the
437///   primitive.
438/// * Non-object receivers (a `Value::String`, a `Value::Null`) return
439///   `None` verbatim via the inherent `Value::get`'s own non-object-
440///   arm behaviour, matching the pre-lift chain's semantics on the
441///   corner where the caller's status blob is malformed.
442///
443/// A future normalization — a per-fleet clamp that rejects negative
444/// counters (the K8s API server never emits them, but a fixture
445/// authoring bug could), a `Value::Number` fallback that accepts
446/// `f64` counters truncated to `i64`, a `checked` overflow arm that
447/// promotes an out-of-range integer to a diagnostic rather than a
448/// silent `None` — lands at THIS ONE substrate primitive and every
449/// downstream Job-status / Deployment-replica / HPA-desired-count
450/// counter reader inherits the upgrade mechanically. No per-site edit
451/// at any of the 3 listed callers or at future consumers (a
452/// Deployment `readyReplicas` projection, an HPA `currentReplicas`
453/// gate, a StatefulSet `updatedReplicas` freshness check).
454///
455/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
456/// two-link `.get(<key>).and_then(|v| v.as_i64())` chain recurred at
457/// three hand-authored sites past the ★★ PRIME-DIRECTIVE ≥ 2
458/// duplication trigger, and is lifted to ONE substrate owner here).
459/// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
460/// regression that drifted the projection axis at ONE site — a swap
461/// of `as_i64` for `as_u64` narrowing the accepted range, a swap of
462/// `.get(<key>)` for `.pointer("<key>")` losing the direct-child
463/// semantics — would silently pass every downstream `JobStatusView`
464/// composition and surface as a wrong counter at operator-facing
465/// diagnostic wording; post-lift the projection lives at ONE typed
466/// owner so a regression surfaces at [`tests::get_i64_null_arm_returns_none`]
467/// / peers rather than as silent operator-facing drift).
468pub trait ValueGetExt {
469    /// Look up `key` on this JSON object and project the returned
470    /// handle to `i64`; returns `None` when the slot is absent, when
471    /// the receiver is not a JSON object, or when the slot's variant
472    /// is not integer-shaped.
473    #[must_use = "a JSON i64 projection that isn't bound swallows the counter entirely"]
474    fn get_i64(&self, key: &str) -> Option<i64>;
475
476    /// Look up `key` on this JSON object and project the returned
477    /// handle to `&str`; returns `None` when the slot is absent, when
478    /// the receiver is not a JSON object, or when the slot's variant
479    /// is not `Value::String`.
480    ///
481    /// String-axis sibling of [`Self::get_i64`] on the same
482    /// `.get(<key>).and_then(|v| v.as_<T>())` READ-chain lift. Pre-lift
483    /// the two-link chain was hand-authored at SEVEN production sites
484    /// across two crates past the ★★ PRIME-DIRECTIVE ≥ 2 duplication
485    /// threshold:
486    ///
487    /// * `tatara-process::status::RenderedResourceCoords::from_json`
488    ///   — FOUR paired reads (`apiVersion`, `kind`, `metadata.name`,
489    ///   `metadata.namespace`) that project the four rendered-resource
490    ///   coordinate slots off a `serde_json::Value` rendered manifest
491    ///   into the typed `RenderedResourceCoords` row; the required
492    ///   three (`apiVersion` / `kind` / `metadata.name`) compose with
493    ///   `.ok_or_else(|| anyhow!("rendered resource missing X"))?
494    ///   .to_string()`, and the optional `metadata.namespace` composes
495    ///   with `.map(str::to_string)`.
496    /// * `tatara-reconciler::ssapply::ready_condition_value` — THREE
497    ///   paired reads (`type`, `status`, `message`) inside the
498    ///   condition-walker's per-condition classifier, each pulling a
499    ///   `Value::String` slot off a K8s Condition object off the
500    ///   `status.conditions[]` array.
501    ///
502    /// All seven sites walked the SAME two-link chain — `.get(<key>)`
503    /// on a `serde_json::Value` already known to be an object, then
504    /// `.and_then(|v| v.as_str())` on the returned `Option<&Value>` —
505    /// and each composed different downstream tails (fallible
506    /// `.ok_or_else(...)?.to_string()`, optional `.map(String::from)`,
507    /// pattern-match `Some("True")` / `Some("False")` / `_`). Post-lift
508    /// each callsite reads `<value>.get_str(<key>)` and the two-link
509    /// READ chain lives at ONE substrate owner here.
510    ///
511    /// ### Naming — `get_str`, not `as_str` or `str_at`
512    ///
513    /// Same discipline as [`Self::get_i64`] — the trait method
514    /// deliberately does NOT collide with `serde_json::Value::as_str`
515    /// (the inherent projection on a single `Value` handle) nor with
516    /// `serde_json::Value::get` (the inherent slot-lookup returning
517    /// `Option<&Value>`). A name collision would let a caller who has
518    /// [`ValueGetExt`] in scope resolve to one of the inherent methods
519    /// by accident (inherent methods win over trait methods in method
520    /// resolution) and silently drop half of the paired chain. The
521    /// `get_str(<key>)` shape names the intent: look up the slot at
522    /// `<key>`, project the returned handle to `&str`, in ONE call.
523    ///
524    /// ### `#[must_use]`
525    ///
526    /// Every pre-lift consumer binds the returned `Option<&str>` into
527    /// a downstream `.ok_or_else(...)?.to_string()` / `.map(String::from)`
528    /// / `.map(str::to_string)` / pattern-match arm. Dropping the
529    /// return silently discards the projection entirely, which is
530    /// never the intended semantic at any of the seven pre-lift
531    /// consumers.
532    ///
533    /// ### Return lifetime
534    ///
535    /// The `&str` borrows the same buffer the underlying
536    /// `Value::String` variant owns; the `Option<&str>` is bounded by
537    /// the receiver's lifetime (`&'_ self`), so a caller holding onto
538    /// the returned slice keeps the receiver borrowed. Matches the
539    /// pre-lift chain's own borrow shape (`v.as_str()` borrows through
540    /// the `&Value`).
541    ///
542    /// A future normalization on the projection — a Unicode
543    /// normalization pass (NFC-folding annotation values), a
544    /// per-fleet trim of leading/trailing whitespace, a rejection of
545    /// empty-string arms as "the caller meant absent" — lands at THIS
546    /// ONE substrate primitive and every downstream `apiVersion` /
547    /// `kind` / `metadata.name` / K8s-condition-string reader
548    /// inherits the upgrade mechanically.
549    ///
550    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
551    /// the two-link `.get(<key>).and_then(|v| v.as_str())` chain
552    /// recurred at SEVEN production sites across two crates past the
553    /// ★★ PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to
554    /// ONE substrate owner here on the string axis of the same
555    /// READ-chain axis-family the `get_i64` sibling opened for the
556    /// integer axis). THEORY.md §II.1 invariant 5 (composition
557    /// preserves proofs — a regression that drifted the projection
558    /// axis at ONE site would silently pass every downstream
559    /// composition and surface as a wrong slot at operator-facing
560    /// diagnostic wording; post-lift the projection lives at ONE
561    /// typed owner so a regression surfaces at
562    /// [`tests::get_str_present_string_slot_returns_the_slice`] /
563    /// peers rather than as silent operator-facing drift).
564    #[must_use = "a JSON &str projection that isn't bound swallows the slot entirely"]
565    fn get_str(&self, key: &str) -> Option<&str>;
566
567    /// Look up `key` on this JSON object and project the returned
568    /// handle to `&Vec<Value>`; returns `None` when the slot is
569    /// absent, when the receiver is not a JSON object, or when the
570    /// slot's variant is not `Value::Array`.
571    ///
572    /// Array-axis sibling of [`Self::get_i64`] + [`Self::get_str`]
573    /// on the same `.get(<key>).and_then(|v| v.as_<T>())` READ-chain
574    /// axis-family. Pre-lift the two-link chain was hand-authored at
575    /// TWO production sites across two crates past the ★★
576    /// PRIME-DIRECTIVE ≥ 2 duplication threshold:
577    ///
578    /// * `tatara-reconciler::ssapply::ready_condition_value` — the
579    ///   tail of the `data.get("status").and_then(|s|
580    ///   s.get("conditions")).and_then(|c| c.as_array())` walker that
581    ///   opens the K8s Condition classifier every DynamicObject
582    ///   readiness probe rides through.
583    /// * `tatara-closed-loop-probe::probe::count_jwks_keys` — the
584    ///   JWKS-response walker that counts issuer-side public keys off
585    ///   the `keys` slot for the closed-loop probe's per-run
586    ///   `jwks_key_count` diagnostic.
587    ///
588    /// Both sites walked the SAME two-link chain — `.get(<key>)` on a
589    /// `serde_json::Value` already known to be an object, then
590    /// `.and_then(|v| v.as_array())` on the returned `Option<&Value>`
591    /// — and composed different downstream tails (`Some(conditions)`
592    /// pattern-match on the reconciler side, `.map(|xs| xs.len() as
593    /// u64)` on the probe side). Post-lift each callsite reads
594    /// `<value>.get_array(<key>)` and the two-link READ chain lives at
595    /// ONE substrate owner here. The probe-side variant additionally
596    /// sheds the pre-lift `.get("keys").cloned()` allocation because
597    /// this primitive borrows through the receiver rather than
598    /// cloning.
599    ///
600    /// ### Naming — `get_array`, not `as_array` or `array_at`
601    ///
602    /// Same discipline as [`Self::get_i64`] + [`Self::get_str`] — the
603    /// trait method deliberately does NOT collide with
604    /// `serde_json::Value::as_array` (the inherent projection on a
605    /// single `Value` handle) nor with `serde_json::Value::get` (the
606    /// inherent slot-lookup returning `Option<&Value>`). A name
607    /// collision would let a caller who has [`ValueGetExt`] in scope
608    /// resolve to one of the inherent methods by accident (inherent
609    /// methods win over trait methods in method resolution) and
610    /// silently drop half of the paired chain. The `get_array(<key>)`
611    /// shape names the intent: look up the slot at `<key>`, project
612    /// the returned handle to `&Vec<Value>`, in ONE call.
613    ///
614    /// ### `#[must_use]`
615    ///
616    /// Every pre-lift consumer binds the returned `Option<&Vec<Value>>`
617    /// into a downstream `let Some(...) = ... else { return ... }`
618    /// short-circuit or a `.map(|xs| xs.len() as u64).unwrap_or(0)`
619    /// counter composition. Dropping the return silently discards the
620    /// projection entirely, which is never the intended semantic at
621    /// either pre-lift consumer.
622    ///
623    /// ### Return lifetime
624    ///
625    /// The `&Vec<Value>` borrows the same buffer the underlying
626    /// `Value::Array` variant owns; the `Option<&Vec<Value>>` is
627    /// bounded by the receiver's lifetime (`&'_ self`), so a caller
628    /// iterating the returned slice keeps the receiver borrowed.
629    /// Matches the pre-lift chain's own borrow shape (`v.as_array()`
630    /// borrows through the `&Value`), and in the probe.rs case
631    /// eliminates the pre-lift `.cloned()` on the intermediate
632    /// `Value` that only existed to sidestep the borrow.
633    ///
634    /// A future normalization on the projection — a rejection of
635    /// empty arrays as "the caller meant absent", an accept-scalar
636    /// coercion (a `Value::String` promoted to a one-element array),
637    /// a per-fleet cap on array length that short-circuits pathological
638    /// payloads — lands at THIS ONE substrate primitive and every
639    /// downstream K8s-Condition classifier / JWKS-array counter /
640    /// future array-slot reader inherits the upgrade mechanically.
641    ///
642    /// Theory anchor: THEORY.md §VI.1 (generation over composition —
643    /// the two-link `.get(<key>).and_then(|v| v.as_array())` chain
644    /// recurred at two production sites across two crates past the ★★
645    /// PRIME-DIRECTIVE ≥ 2 duplication trigger, and is lifted to ONE
646    /// substrate owner here on the array axis of the same READ-chain
647    /// axis-family the `get_i64` + `get_str` siblings already own).
648    /// THEORY.md §II.1 invariant 5 (composition preserves proofs — a
649    /// regression that drifted the projection axis at ONE site would
650    /// silently pass every downstream composition and surface as a
651    /// wrong slot at operator-facing diagnostic wording; post-lift the
652    /// projection lives at ONE typed owner so a regression surfaces
653    /// at [`tests::get_array_present_array_slot_returns_the_slice`] /
654    /// peers rather than as silent operator-facing drift).
655    #[must_use = "a JSON array projection that isn't bound swallows the slot entirely"]
656    fn get_array(&self, key: &str) -> Option<&Vec<Value>>;
657}
658
659impl ValueGetExt for Value {
660    #[inline]
661    fn get_i64(&self, key: &str) -> Option<i64> {
662        self.get(key).and_then(Value::as_i64)
663    }
664
665    #[inline]
666    fn get_str(&self, key: &str) -> Option<&str> {
667        self.get(key).and_then(Value::as_str)
668    }
669
670    #[inline]
671    fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
672        self.get(key).and_then(Value::as_array)
673    }
674}
675
676/// Receiver-shape widening of the READ-projection axis-family — the
677/// same three methods extended from `Value` (the `Value::Object` arm's
678/// walker) to `Map<String, Value>` (the object interior itself),
679/// closing the receiver-shape gap so a caller who already holds an
680/// `&Map<String, Value>` handle (via `.as_object().unwrap()`, via
681/// [`ValueObjectExt::as_object_mut_or`], via the two `JsonMap*Ext`
682/// siblings' returns, or via a helper like `ssapply::ownership_kv_pair`
683/// that composes and returns a `Map` directly) reaches the SAME
684/// `get_i64` / `get_str` / `get_array` methods without a
685/// `Value::Object(m)` rewrap detour.
686///
687/// Pre-lift the `.get(<key>).and_then(Value::as_<T>)` two-link chain
688/// was hand-authored at 30 `Map<String, Value>`-receiver sites past the
689/// ★★ PRIME-DIRECTIVE ≥ 2 duplication threshold — 19 in
690/// `tatara-reconciler::patch` tests (the phase-status wire-shape pins
691/// walking `obj = v.as_object().unwrap()` and `metadata = obj.get(
692/// "metadata").and_then(Value::as_object).unwrap()` receivers) plus
693/// 11 in `tatara-reconciler::ssapply` tests (the ownership-tag +
694/// composed-coord pins walking the `Map` handles returned by
695/// `ownership_annotations` / `ownership_labels` /
696/// `ownership_annotations_by_coord`). Post-lift each callsite reads
697/// `<map>.get_str(<key>)` / `<map>.get_array(<key>)` and the READ
698/// chain rides through the SAME substrate owner the `Value`-receiver
699/// callers already threaded through.
700///
701/// The axis-family invariant (a caller who imports `ValueGetExt`
702/// reaches every axis through the same trait handle — pinned at
703/// [`tests::get_array_axis_family_reaches_i64_str_and_array_through_one_trait_import`]
704/// and its `get_str` sibling) extends verbatim to the `Map` receiver:
705/// a single `use tatara_process::json_object::ValueGetExt;` unlocks
706/// every axis on both receiver shapes. A future new axis (e.g. a
707/// `get_bool` for `Value::Bool` slots) adds one method on the trait
708/// and inherits both impls; there is no separate `MapGetExt` peer to
709/// keep in sync.
710///
711/// Theory anchor: THEORY.md §VI.1 (generation over composition — the
712/// two-link chain recurred at 30 `Map`-receiver sites past the ★★
713/// PRIME-DIRECTIVE ≥ 2 duplication trigger, and rides through the
714/// same substrate owner the pre-existing `Value`-receiver impl above
715/// already pinned). THEORY.md §II.1 invariant 5 (composition preserves
716/// proofs — the receiver-shape widening carries the axis-family
717/// invariant across without splitting it into two traits).
718impl ValueGetExt for Map<String, Value> {
719    #[inline]
720    fn get_i64(&self, key: &str) -> Option<i64> {
721        self.get(key).and_then(Value::as_i64)
722    }
723
724    #[inline]
725    fn get_str(&self, key: &str) -> Option<&str> {
726        self.get(key).and_then(Value::as_str)
727    }
728
729    #[inline]
730    fn get_array(&self, key: &str) -> Option<&Vec<Value>> {
731        self.get(key).and_then(Value::as_array)
732    }
733}
734
735#[cfg(test)]
736mod tests {
737    use super::*;
738    use serde_json::json;
739
740    // ─── ValueObjectExt::as_object_mut_or substrate pins ─────────────
741    //
742    // Fail-before-pass-after granularity: the `ValueObjectExt::
743    // as_object_mut_or` trait method did not exist before this commit,
744    // so each test below fails to compile pre-lift. Post-lift they
745    // collectively pin the object-guard shape at ONE substrate owner —
746    // a regression that drifts the error message wording, swaps the
747    // `<slot>` slot, wraps the source in a chain-form `source` (which
748    // would change `Display` output when downstream tracing formatters
749    // interpolate `{e}` rather than the chain-walking `{e:#}`), or
750    // promotes the pass-through arm to synthesis (a `None → Ok(&mut
751    // Map::default())` fallthrough that silently swallows a mistyped
752    // slot) surfaces HERE rather than as silent operator-facing skew
753    // across the three `ssapply.rs` pre-lift consumers whose log
754    // output already encoded the flat `"<slot> is not an object"`
755    // shape.
756
757    #[test]
758    fn as_object_mut_or_object_arm_returns_the_inner_map_mutably() {
759        // Ok-arm invariant: a `Value::Object` handle threaded through
760        // `as_object_mut_or("<slot>")` MUST return `Ok(&mut Map)`
761        // whose interior is the SAME `serde_json::Map` the underlying
762        // `serde_json::Value::as_object_mut` would return — no clone,
763        // no reshape, no synthesis. The `&mut` return is load-bearing
764        // at every consumer (each threads a downstream `.entry(...).
765        // or_insert_with(...)` / `.insert(...)` mutation onto the
766        // returned reference), so a regression that returned a fresh
767        // owned `Map` here would silently drop every downstream write.
768        let mut v = json!({ "existing_key": "existing_value" });
769        let map = v.as_object_mut_or("resource").expect("Value::Object");
770        map.insert("new_key".to_string(), json!("new_value"));
771        assert_eq!(v["existing_key"], "existing_value");
772        assert_eq!(v["new_key"], "new_value");
773    }
774
775    #[test]
776    fn as_object_mut_or_null_arm_errors_with_pre_lift_display_bytewise() {
777        // Byte-shape parity pin: the wrap output of `as_object_mut_or
778        // ("<slot>")` on a `Value::Null` handle MUST be `Display`-
779        // identical to the pre-lift hand-authored `.as_object_mut().
780        // ok_or_else(|| anyhow!("<slot> is not an object"))?` chain.
781        // A regression that inserted a synonym (`"<slot> is not a
782        // JSON object"`), reshaped the slot position (`"not an
783        // object: <slot>"`), or dropped the leading `<slot>` slot
784        // surfaces HERE rather than as silent drift at every
785        // downstream log-output consumer.
786        let mut v = Value::Null;
787        let err = v.as_object_mut_or("resource").unwrap_err();
788        assert_eq!(format!("{err}"), "resource is not an object");
789    }
790
791    #[test]
792    fn as_object_mut_or_array_arm_errors_with_pre_lift_display_bytewise() {
793        // Sibling to the null-arm byte-shape pin — a mistyped
794        // `metadata` slot authored as a JSON array (kubectl accepts
795        // `metadata: []` in a YAML manifest with no schema, though the
796        // apiserver later rejects it) surfaces the same guard error.
797        // Pins the "non-object variants ALL error via the same wire
798        // format" invariant — a regression that special-cased the
799        // array variant (returning a fresh empty map, silently
800        // coercing) surfaces HERE.
801        let mut v = json!(["not", "an", "object"]);
802        let err = v.as_object_mut_or("metadata").unwrap_err();
803        assert_eq!(format!("{err}"), "metadata is not an object");
804    }
805
806    #[test]
807    fn as_object_mut_or_string_arm_errors_with_pre_lift_display_bytewise() {
808        // Sibling to the null / array pins — a mistyped `annotations`
809        // slot authored as a JSON string (a common apiserver-layer
810        // authoring bug in kubectl-generated manifests where a
811        // stringified JSON object leaks through) surfaces the same
812        // guard error. Pins the "every non-object variant errors via
813        // the same wire format" invariant across the full
814        // `serde_json::Value` sum.
815        let mut v = json!("stringified");
816        let err = v.as_object_mut_or("annotations").unwrap_err();
817        assert_eq!(format!("{err}"), "annotations is not an object");
818    }
819
820    #[test]
821    fn as_object_mut_or_threads_the_slot_slug_verbatim_across_all_three_pre_lift_labels() {
822        // Cross-slot coherence pin: the three pre-lift consumers in
823        // `tatara-reconciler::ssapply` stamped THREE distinct slot
824        // slugs (`"resource"` / `"metadata"` / `"annotations"`), and
825        // the wrap-shape MUST honor each one verbatim as the leading
826        // slot in the `Display` output. A regression that hard-coded
827        // one slug (say `"resource"`) across every callsite would
828        // pass the first pin above and fail HERE — the three
829        // downstream error-stream greps operators run to bisect a
830        // "which SSA-time mutation faulted" alert would ALL collapse
831        // to the same slug.
832        for slot in ["resource", "metadata", "annotations"] {
833            let mut v = Value::Null;
834            let err = v.as_object_mut_or(slot).unwrap_err();
835            assert_eq!(format!("{err}"), format!("{slot} is not an object"));
836        }
837    }
838
839    #[test]
840    fn as_object_mut_or_object_arm_matches_inherent_as_object_mut_bytewise() {
841        // Cross-substrate coherence pin: on the Ok arm the trait
842        // method MUST return the SAME `&mut Map` the inherent
843        // `serde_json::Value::as_object_mut` would — no diverging
844        // view, no clone, no key-order reshape. A regression that
845        // introduced a normalization pass here (sorting keys,
846        // stripping a null-valued entry, coercing a nested string
847        // to a JSON scalar) would surface as silent per-consumer
848        // schema drift at the SSA-time mutation — an ownerReferences
849        // append that no longer landed in the same slot the apiserver
850        // reads, an annotations insert whose key ordering diverged
851        // from kubectl's canonical form.
852        let mut via_trait = json!({ "key": "value", "nested": { "inner": 1 } });
853        let mut via_inherent = via_trait.clone();
854        assert_eq!(
855            via_trait
856                .as_object_mut_or("resource")
857                .expect("Value::Object")
858                .clone(),
859            via_inherent.as_object_mut().expect("Value::Object").clone(),
860        );
861    }
862
863    // ─── JsonMapStrExt::insert_str substrate pins ─────────────────
864    //
865    // Fail-before-pass-after granularity: the `JsonMapStrExt::insert_str`
866    // trait method did not exist before this commit, so each test below
867    // fails to compile pre-lift. Post-lift they collectively pin the
868    // string-slot write shape at ONE substrate owner — a regression that
869    // dropped the `Value::String` wrap (silently coercing to a bare
870    // `Value::from(&str)` — byte-identical in the `Object` arm today but
871    // divergent for any future non-`&str` numeric caller who reached for
872    // `insert_str(k, n.to_string())`), swapped the key + value slot
873    // orientation, or drifted the return semantics from the inherent
874    // `Map::insert` (which returns the previous value on overwrite —
875    // load-bearing at any future caller that inspects the return) would
876    // surface HERE rather than as silent per-emit skew across the
877    // thirteen pre-lift `ssapply` + `render` + `edges` consumers.
878
879    #[test]
880    fn insert_str_new_key_returns_none_and_stamps_value_string() {
881        // New-key arm: matches inherent `Map::insert` return
882        // semantics — `None` for a fresh key — and stamps a
883        // `Value::String` (NOT `Value::from(&str)`, though they're
884        // byte-identical today) at the slot.
885        let mut m = Map::new();
886        let prev = m.insert_str("key", "value");
887        assert!(prev.is_none(), "new key returns None");
888        assert_eq!(m.get("key"), Some(&Value::String("value".to_string())));
889        assert!(matches!(m.get("key"), Some(Value::String(_))));
890    }
891
892    #[test]
893    fn insert_str_overwrite_returns_prior_value_and_stamps_new() {
894        // Overwrite arm: matches inherent `Map::insert` return
895        // semantics — `Some(prev)` on overwrite. Load-bearing for
896        // any future consumer that inspects the return to detect a
897        // slot collision (a fleet-wide sweep that flagged a
898        // duplicate SSA-time annotation stamp, for example).
899        let mut m = Map::new();
900        m.insert_str("key", "old");
901        let prev = m.insert_str("key", "new");
902        assert_eq!(prev, Some(Value::String("old".to_string())));
903        assert_eq!(m.get("key"), Some(&Value::String("new".to_string())));
904    }
905
906    #[test]
907    fn insert_str_accepts_str_and_owned_string_at_both_slots() {
908        // Composability pin: both slots MUST accept `&str` and
909        // `String` interchangeably — the pre-lift callsite inventory
910        // mixes both (SSA-time `annotations::PID` static + a
911        // `pid.to_string()` runtime String at the value slot;
912        // `spec.insert("interval".into(), Value::String("1m".into()))`
913        // with two `&str` slots). A regression that constrained
914        // either slot to one shape would break the callsite parity
915        // that motivated this substrate primitive.
916        let mut m1 = Map::new();
917        m1.insert_str("a", "b");
918        let mut m2 = Map::new();
919        m2.insert_str(String::from("a"), String::from("b"));
920        let mut m3 = Map::new();
921        m3.insert_str("a", String::from("b"));
922        let mut m4 = Map::new();
923        m4.insert_str(String::from("a"), "b");
924        assert_eq!(m1, m2);
925        assert_eq!(m2, m3);
926        assert_eq!(m3, m4);
927    }
928
929    #[test]
930    fn insert_str_matches_pre_lift_hand_authored_shape_bytewise() {
931        // Byte-shape parity pin: `insert_str(k, v)` MUST emit the
932        // SAME `Map` entry the pre-lift hand-authored `.insert(
933        // <k>.into(), Value::String(<v>.into()))` chain produced.
934        // Sweeps the four (str × String) × (str × String) key/value
935        // shape quadrants so a regression at the primitive that
936        // broke the byte identity with the pre-lift shape at ONE
937        // quadrant surfaces here rather than as a subtle per-emit
938        // divergence at that quadrant.
939        for (k_str, v_str) in [("a", "b"), ("x", ""), ("", "y"), ("", "")] {
940            // (str, str) quadrant
941            let mut via_primitive = Map::new();
942            via_primitive.insert_str(k_str, v_str);
943            let mut via_pre_lift = Map::new();
944            via_pre_lift.insert(k_str.into(), Value::String(v_str.into()));
945            assert_eq!(via_primitive, via_pre_lift);
946
947            // (String, String) quadrant
948            let mut via_primitive = Map::new();
949            via_primitive.insert_str(String::from(k_str), String::from(v_str));
950            let mut via_pre_lift = Map::new();
951            via_pre_lift.insert(String::from(k_str), Value::String(String::from(v_str)));
952            assert_eq!(via_primitive, via_pre_lift);
953        }
954    }
955
956    #[test]
957    fn insert_str_empty_value_stamps_empty_string_not_null() {
958        // Semantic pin: an empty value slot MUST stamp
959        // `Value::String("")`, NEVER `Value::Null`. Load-bearing at
960        // any callsite that stamps a placeholder empty-string
961        // annotation (say a `content_hash` slot pre-derive) where a
962        // `Null` slot would fail-loud at the K8s apiserver's
963        // annotation-value type check.
964        let mut m = Map::new();
965        m.insert_str("empty", "");
966        assert_eq!(m.get("empty"), Some(&Value::String(String::new())));
967        assert!(!matches!(m.get("empty"), Some(Value::Null)));
968    }
969
970    // ─── JsonMapObjectEntryExt::object_slot_mut_or substrate pins ─────
971    //
972    // Fail-before-pass-after granularity: the
973    // `JsonMapObjectEntryExt::object_slot_mut_or` trait method did not
974    // exist before this commit, so each test below fails to compile
975    // pre-lift. Post-lift they collectively pin the compound
976    // seed-then-guard shape at ONE substrate owner — a regression that
977    // dropped the seed step (leaving an absent slot to fall through the
978    // guard as `None → Err`), skipped the guard step (silently returning
979    // an `&mut Value` when the existing slot is a non-object variant),
980    // drifted the entry-key slot vs. the guard-error slot (a copy-paste
981    // typo that stamped `"metadata"` into the entry and `"metadatas"`
982    // into the guard error message), or drifted the empty-seed shape
983    // (a `Value::Null` fallback where `Value::Object(Map::new())` is
984    // load-bearing at the downstream `.entry(...).or_insert_with(...)`
985    // / `.insert(...)` mutation) would surface HERE rather than as
986    // silent per-emit skew across the two pre-lift `ssapply.rs`
987    // consumers.
988
989    #[test]
990    fn object_slot_mut_or_absent_slot_seeds_empty_object_and_returns_it() {
991        // Absent-slot arm: the pre-lift `.entry(<slot>).or_insert_with
992        // (|| Value::Object(Default::default()))` step MUST seed the
993        // slot with an EMPTY `Value::Object` when the slot is not
994        // present in the parent map. The returned handle is the fresh
995        // empty map, MUTABLY, so a downstream `.insert(...)` writes
996        // land in the parent map's `<slot>` object post-return.
997        let mut parent = Map::new();
998        {
999            let child = parent
1000                .object_slot_mut_or("metadata")
1001                .expect("absent slot seeds an object");
1002            assert!(child.is_empty(), "fresh-seeded slot is an empty object");
1003            child.insert("name".into(), Value::String("demo".into()));
1004        }
1005        // The write landed in the parent map's metadata slot.
1006        assert_eq!(parent["metadata"]["name"], "demo");
1007        assert!(matches!(parent.get("metadata"), Some(Value::Object(_))));
1008    }
1009
1010    #[test]
1011    fn object_slot_mut_or_present_object_slot_returns_existing_interior_mutably() {
1012        // Present-object-slot arm: when the slot is already populated
1013        // with a `Value::Object`, the primitive MUST return the
1014        // EXISTING map interior mutably — no synthesis, no reshape, no
1015        // key-order rewrite. The downstream `.insert(...)` writes MUST
1016        // merge into the pre-existing keys rather than replace them.
1017        let mut parent = Map::new();
1018        parent.insert(
1019            "metadata".into(),
1020            serde_json::json!({ "existing_key": "existing_value" }),
1021        );
1022        {
1023            let child = parent
1024                .object_slot_mut_or("metadata")
1025                .expect("present-object slot returns Ok");
1026            assert_eq!(
1027                child.get("existing_key"),
1028                Some(&Value::String("existing_value".into()))
1029            );
1030            child.insert("new_key".into(), Value::String("new_value".into()));
1031        }
1032        assert_eq!(parent["metadata"]["existing_key"], "existing_value");
1033        assert_eq!(parent["metadata"]["new_key"], "new_value");
1034    }
1035
1036    #[test]
1037    fn object_slot_mut_or_present_non_object_slot_errors_with_pre_lift_display() {
1038        // Fail-loud arm: when the slot is present but holds a non-
1039        // object variant (a `Value::String` from a hand-authored
1040        // YAML manifest where `metadata: "malformed"` slipped past
1041        // kubectl's schema check), the primitive MUST fail with a
1042        // `Display` byte-identical to the pre-lift
1043        // `.as_object_mut_or(<slot>)?` guard — the sibling
1044        // [`ValueObjectExt::as_object_mut_or`] guard's wire format.
1045        // A regression that special-cased this arm (overwriting the
1046        // slot with a fresh empty object, silently coercing) would
1047        // silently swallow the operator's authoring error at the
1048        // SSA-time re-injection step.
1049        let mut parent = Map::new();
1050        parent.insert("metadata".into(), Value::String("malformed".into()));
1051        let err = parent.object_slot_mut_or("metadata").unwrap_err();
1052        assert_eq!(format!("{err}"), "metadata is not an object");
1053    }
1054
1055    #[test]
1056    fn object_slot_mut_or_threads_the_slot_slug_verbatim_across_both_pre_lift_labels() {
1057        // Cross-slot coherence pin: the TWO pre-lift consumers in
1058        // `tatara-reconciler::ssapply` stamped TWO distinct slot slugs
1059        // (`"metadata"` at the resource root, `"annotations"` at the
1060        // metadata child), and the wrap-shape MUST honor each one
1061        // verbatim as the leading slot in the `Display` output. A
1062        // regression that hard-coded one slug across every callsite
1063        // would pass the fail-loud pin above (on the `"metadata"` slug)
1064        // and fail HERE — the two downstream error-stream greps
1065        // operators run to bisect a "which SSA-time slot mutation
1066        // faulted" alert would ALL collapse to the same slug, hiding
1067        // whether the fault was at the resource-root object walk or
1068        // the metadata-child annotations walk.
1069        for slot in ["metadata", "annotations"] {
1070            let mut parent = Map::new();
1071            parent.insert(slot.into(), Value::Null);
1072            let err = parent.object_slot_mut_or(slot).unwrap_err();
1073            assert_eq!(format!("{err}"), format!("{slot} is not an object"));
1074        }
1075    }
1076
1077    #[test]
1078    fn object_slot_mut_or_present_empty_object_returns_existing_reference_not_synthesized() {
1079        // Precedence pin: a present slot holding an EMPTY
1080        // `Value::Object` MUST return the pre-existing empty map
1081        // interior — not a freshly-synthesized replacement. The
1082        // pre-lift `.entry(<slot>).or_insert_with(||...)` step's
1083        // short-circuit on the present-slot arm skips the closure
1084        // entirely; a regression that always evaluated the closure
1085        // (unconditionally overwriting an existing empty-object slot
1086        // with a fresh empty object) would type-check silently at
1087        // every callsite AND write byte-identical JSON at the empty-
1088        // slot corner, but it would break a hypothetical future
1089        // consumer that reached the primitive on a map whose slot
1090        // was seeded upstream with metadata (a caller intending to
1091        // preserve any keys the parent-composer already dropped in).
1092        let mut parent = Map::new();
1093        parent.insert("metadata".into(), Value::Object(Map::new()));
1094        let addr_before = parent.get("metadata").unwrap() as *const Value;
1095        {
1096            let _child = parent.object_slot_mut_or("metadata").unwrap();
1097        }
1098        let addr_after = parent.get("metadata").unwrap() as *const Value;
1099        assert_eq!(
1100            addr_before, addr_after,
1101            "present empty-object slot must return the pre-existing reference, not a fresh synthesis",
1102        );
1103    }
1104
1105    #[test]
1106    fn object_slot_mut_or_matches_pre_lift_hand_authored_compound_shape_bytewise() {
1107        // Byte-shape parity pin: `object_slot_mut_or(<slot>)?` MUST
1108        // produce the SAME `&mut Map` (and, on the non-object arm, the
1109        // SAME `Display`-shaped error) the pre-lift 3-line `.entry
1110        // (<slot>).or_insert_with(|| Value::Object(Default::default()))
1111        // .as_object_mut_or(<slot>)?` chain produced. Sweeps the three
1112        // pre-lift-reachable input corners (absent slot / present
1113        // object / present non-object) so a regression at the primitive
1114        // that broke byte identity with the pre-lift chain at ONE
1115        // corner surfaces here rather than as a subtle per-emit
1116        // divergence.
1117        for slot in ["metadata", "annotations"] {
1118            // (1) Absent-slot corner: both routes seed empty-object at
1119            //     the slot AND return the same empty map interior.
1120            let mut via_primitive = Map::new();
1121            let mut via_pre_lift = Map::new();
1122            {
1123                let _ = via_primitive.object_slot_mut_or(slot).unwrap();
1124                let _ = via_pre_lift
1125                    .entry(slot.to_string())
1126                    .or_insert_with(|| Value::Object(Map::new()))
1127                    .as_object_mut_or(slot)
1128                    .unwrap();
1129            }
1130            assert_eq!(via_primitive, via_pre_lift);
1131
1132            // (2) Present-object corner: both routes read back the
1133            //     same pre-populated interior mutably.
1134            let mut via_primitive = Map::new();
1135            via_primitive.insert(slot.into(), serde_json::json!({ "k": "v" }));
1136            let mut via_pre_lift = via_primitive.clone();
1137            {
1138                let a = via_primitive.object_slot_mut_or(slot).unwrap();
1139                let b = via_pre_lift
1140                    .entry(slot.to_string())
1141                    .or_insert_with(|| Value::Object(Map::new()))
1142                    .as_object_mut_or(slot)
1143                    .unwrap();
1144                assert_eq!(a, b);
1145            }
1146
1147            // (3) Present-non-object corner: both routes fail loud
1148            //     with the same wire-format Display shape.
1149            let mut via_primitive = Map::new();
1150            via_primitive.insert(slot.into(), Value::Bool(true));
1151            let mut via_pre_lift = via_primitive.clone();
1152            let err_primitive = via_primitive.object_slot_mut_or(slot).unwrap_err();
1153            let err_pre_lift = via_pre_lift
1154                .entry(slot.to_string())
1155                .or_insert_with(|| Value::Object(Map::new()))
1156                .as_object_mut_or(slot)
1157                .unwrap_err();
1158            assert_eq!(format!("{err_primitive}"), format!("{err_pre_lift}"));
1159        }
1160    }
1161
1162    // ─── ValueGetExt::get_i64 substrate pins ─────────────────────────
1163    //
1164    // Fail-before-pass-after granularity: the `ValueGetExt::get_i64`
1165    // trait method did not exist before this commit, so each test below
1166    // fails to compile pre-lift. Post-lift they collectively pin the
1167    // paired READ-shape at ONE substrate owner — a regression that
1168    // narrowed the projection to `as_u64` (silently losing every
1169    // negative counter K8s fixtures can carry for a JSON authoring
1170    // bug), swapped the slot lookup to `.pointer(<key>)` (losing the
1171    // direct-child semantics), promoted a present-but-non-integer
1172    // corner to `Some(0)` (silently paving over a malformed status
1173    // blob), or drifted the receiver-non-object arm from `None → Some(default)`
1174    // (silently synthesising a zero counter on a null status blob)
1175    // surfaces HERE rather than as silent operator-facing skew across
1176    // the three `boundary.rs::fetch_job_status` pre-lift consumers
1177    // whose JobStatusView row initialised at `Default::default()` and
1178    // conditionally overwrote each field on `Some(i64)`.
1179
1180    #[test]
1181    fn get_i64_present_integer_slot_returns_the_value() {
1182        // Primary Ok-arm invariant: a `Value::Number(i)` present at the
1183        // slot projects to `Some(i)`. Sweeps the three representative
1184        // counters every pre-lift `JobStatusView` field carried (a
1185        // completed Job's `succeeded=1`, a failed Job's `failed=3`, a
1186        // freshly-scheduled Job's `active=5`) so a regression at ONE
1187        // counter axis surfaces here rather than at the downstream
1188        // diagnostic.
1189        let status = json!({ "succeeded": 1, "failed": 3, "active": 5 });
1190        assert_eq!(status.get_i64("succeeded"), Some(1));
1191        assert_eq!(status.get_i64("failed"), Some(3));
1192        assert_eq!(status.get_i64("active"), Some(5));
1193    }
1194
1195    #[test]
1196    fn get_i64_absent_slot_returns_none() {
1197        // Absent-slot corner: a fresh `batch/v1::Job` before its
1198        // controller has stamped any counter into `status` (the JSON
1199        // is `{}` or missing the counter key). Every pre-lift consumer
1200        // routed this corner through the `if let Some(...)` guard so
1201        // the `JobStatusView` field kept its `Default::default()` `0`
1202        // seed. A regression that returned `Some(0)` on the absent
1203        // corner would collapse the "not yet reported" ↔ "reported
1204        // zero" distinction the K8s status protocol keeps.
1205        let status = json!({});
1206        assert_eq!(status.get_i64("succeeded"), None);
1207        assert_eq!(status.get_i64("any_missing_key"), None);
1208    }
1209
1210    #[test]
1211    fn get_i64_present_but_non_integer_slot_returns_none() {
1212        // Present-but-non-integer corner: a `Value::String`, a
1213        // `Value::Bool`, a `Value::Object`, or a `Value::Array` at the
1214        // slot ALL fall through to `None` — matches the pre-lift
1215        // `.and_then(|v| v.as_i64())` chain exactly. A regression that
1216        // promoted a `Value::String("1")` to `Some(1)` (adding a
1217        // parse-string fallback) would silently accept a malformed
1218        // status blob whose author stringified a counter.
1219        let status = json!({
1220            "stringy": "1",
1221            "boolean": true,
1222            "object": {},
1223            "array": [],
1224            "null_valued": null,
1225        });
1226        assert_eq!(status.get_i64("stringy"), None);
1227        assert_eq!(status.get_i64("boolean"), None);
1228        assert_eq!(status.get_i64("object"), None);
1229        assert_eq!(status.get_i64("array"), None);
1230        assert_eq!(status.get_i64("null_valued"), None);
1231    }
1232
1233    #[test]
1234    fn get_i64_negative_counter_survives_the_projection() {
1235        // Negative-integer corner: `as_i64` accepts negatives; `as_u64`
1236        // does not. A regression that narrowed the projection to
1237        // `as_u64` under a mistaken "K8s counters are always non-
1238        // negative" refactor would silently drop every negative
1239        // counter a JSON authoring bug could stamp — hiding the bug
1240        // rather than surfacing it as a counter the diagnostic reports
1241        // verbatim.
1242        let status = json!({ "n": -1 });
1243        assert_eq!(status.get_i64("n"), Some(-1));
1244    }
1245
1246    #[test]
1247    fn get_i64_non_object_receiver_returns_none_verbatim() {
1248        // Non-object receiver corner: a caller who reached this
1249        // primitive on a `Value::Null` / `Value::Bool` / `Value::Array`
1250        // handle (a malformed fetch response, an upstream default-value
1251        // fallback) MUST get `None` back rather than a panic or a
1252        // synthesized `Some(default)`. Matches the pre-lift chain's
1253        // behaviour: `Value::get` on a non-object receiver returns
1254        // `None`, `and_then` short-circuits.
1255        assert_eq!(Value::Null.get_i64("any"), None);
1256        assert_eq!(Value::Bool(true).get_i64("any"), None);
1257        assert_eq!(json!([1, 2, 3]).get_i64("any"), None);
1258        assert_eq!(json!("scalar").get_i64("any"), None);
1259    }
1260
1261    #[test]
1262    fn get_i64_matches_pre_lift_hand_authored_chain_shape() {
1263        // Byte-shape parity pin: `<value>.get_i64(<key>)` MUST return
1264        // the SAME `Option<i64>` the pre-lift hand-authored
1265        // `.get(<key>).and_then(|v| v.as_i64())` chain produced.
1266        // Sweeps the six pre-lift-reachable input corners (the three
1267        // "value present" + three "value absent/malformed" arms every
1268        // fetch_job_status callsite reached) so a regression at the
1269        // primitive that broke byte identity with the pre-lift chain at
1270        // ONE corner surfaces here rather than as a per-counter
1271        // divergence at the fetched-Job projection.
1272        let status = json!({
1273            "succeeded": 2,
1274            "failed": 0,
1275            "active": 7,
1276            "stringy": "1",
1277            "null_valued": null,
1278        });
1279        for key in [
1280            "succeeded",
1281            "failed",
1282            "active",
1283            "stringy",
1284            "null_valued",
1285            "missing",
1286        ] {
1287            let via_primitive = status.get_i64(key);
1288            let via_pre_lift = status.get(key).and_then(|v| v.as_i64());
1289            assert_eq!(
1290                via_primitive, via_pre_lift,
1291                "corner `{key}` must round-trip through both shapes",
1292            );
1293        }
1294    }
1295
1296    #[test]
1297    fn get_i64_composes_with_unwrap_or_default_at_default_seed_shape() {
1298        // Downstream composition pin: the canonical caller shape
1299        // post-lift is `<status>.get_i64(<key>).unwrap_or_default()` —
1300        // matches the pre-lift `JobStatusView::default()` seed +
1301        // conditional `if let Some(n)` write pattern. A regression that
1302        // reshaped the return form (an `i64` bare default, a
1303        // `Result<i64, _>` fallible arm) would break this composition.
1304        let status = json!({ "succeeded": 4 });
1305        // Absent slot composes to the type default (0 for i64).
1306        assert_eq!(status.get_i64("missing").unwrap_or_default(), 0_i64);
1307        // Present slot composes to the projected counter.
1308        assert_eq!(status.get_i64("succeeded").unwrap_or_default(), 4_i64);
1309    }
1310
1311    // ─── ValueGetExt::get_str substrate pins ─────────────────────────
1312    //
1313    // Fail-before-pass-after granularity: the `ValueGetExt::get_str`
1314    // trait method did not exist before this commit, so each test below
1315    // fails to compile pre-lift. Post-lift they collectively pin the
1316    // paired READ-shape at ONE substrate owner — a regression that
1317    // narrowed the projection to the wrong variant (accepting
1318    // `Value::Number`-stringified slots via a fallback, or accepting
1319    // `Value::Null` as `Some("")`), swapped the slot lookup to
1320    // `.pointer(<key>)` (losing the direct-child semantics), promoted
1321    // an absent slot to `Some("")` (silently paving over a missing
1322    // required slot), or drifted the receiver-non-object arm from
1323    // `None` (silently synthesising an empty string on a null status
1324    // blob) surfaces HERE rather than as silent operator-facing skew
1325    // across the SEVEN pre-lift consumers (`status::from_json`'s four
1326    // rendered-resource coordinate reads + `ssapply::ready_condition_value`'s
1327    // three K8s Condition slot reads).
1328
1329    #[test]
1330    fn get_str_present_string_slot_returns_the_slice() {
1331        // Primary Ok-arm invariant: a `Value::String(s)` present at the
1332        // slot projects to `Some(s.as_str())`. Sweeps the four
1333        // representative slots the pre-lift `RenderedResourceCoords::
1334        // from_json` consumer walked (`apiVersion`, `kind`,
1335        // `metadata.name`, `metadata.namespace`) so a regression at
1336        // ONE axis surfaces here rather than at the downstream
1337        // typed row's coordinate.
1338        let manifest = json!({
1339            "apiVersion": "helm.toolkit.fluxcd.io/v2",
1340            "kind": "HelmRelease",
1341            "name": "demo-app",
1342            "namespace": "demo",
1343        });
1344        assert_eq!(
1345            manifest.get_str("apiVersion"),
1346            Some("helm.toolkit.fluxcd.io/v2"),
1347        );
1348        assert_eq!(manifest.get_str("kind"), Some("HelmRelease"));
1349        assert_eq!(manifest.get_str("name"), Some("demo-app"));
1350        assert_eq!(manifest.get_str("namespace"), Some("demo"));
1351    }
1352
1353    #[test]
1354    fn get_str_absent_slot_returns_none() {
1355        // Absent-slot corner: a rendered manifest whose author forgot
1356        // the `apiVersion` slot (a common authoring bug) MUST return
1357        // `None` so `RenderedResourceCoords::from_json` fails loud
1358        // rather than silently synthesising an empty apiVersion. A
1359        // regression that returned `Some("")` on the absent corner
1360        // would collapse the "not authored" ↔ "authored empty"
1361        // distinction the fail-loud gate depends on.
1362        let manifest = json!({ "kind": "HelmRelease" });
1363        assert_eq!(manifest.get_str("apiVersion"), None);
1364        assert_eq!(manifest.get_str("any_missing_key"), None);
1365    }
1366
1367    #[test]
1368    fn get_str_present_but_non_string_slot_returns_none() {
1369        // Present-but-non-string corner: a `Value::Number`,
1370        // `Value::Bool`, `Value::Object`, `Value::Array`, or
1371        // `Value::Null` at the slot ALL fall through to `None` —
1372        // matches the pre-lift `.and_then(|v| v.as_str())` chain
1373        // exactly. A regression that stringified a `Value::Number`
1374        // (adding a `to_string()` fallback) would silently accept a
1375        // malformed manifest whose author numeric-typed a
1376        // conventionally-string slot.
1377        let manifest = json!({
1378            "numeric": 1,
1379            "boolean": true,
1380            "object": {},
1381            "array": [],
1382            "null_valued": null,
1383        });
1384        assert_eq!(manifest.get_str("numeric"), None);
1385        assert_eq!(manifest.get_str("boolean"), None);
1386        assert_eq!(manifest.get_str("object"), None);
1387        assert_eq!(manifest.get_str("array"), None);
1388        assert_eq!(manifest.get_str("null_valued"), None);
1389    }
1390
1391    #[test]
1392    fn get_str_empty_string_slot_survives_the_projection() {
1393        // Empty-string corner: a `Value::String("")` present at the
1394        // slot MUST project to `Some("")` — matches the pre-lift
1395        // `.and_then(|v| v.as_str())` chain exactly, keeping the
1396        // "authored empty" arm distinct from the "not authored" arm
1397        // upstream. A regression that promoted `Some("")` to `None`
1398        // under a "reject empty strings" refactor would silently
1399        // collapse the two arms and turn a valid empty `metadata.
1400        // namespace` (a cluster-scoped resource) into a fail-loud
1401        // error at the required-slot gates.
1402        let manifest = json!({ "namespace": "" });
1403        assert_eq!(manifest.get_str("namespace"), Some(""));
1404    }
1405
1406    #[test]
1407    fn get_str_non_object_receiver_returns_none_verbatim() {
1408        // Non-object receiver corner: a caller who reached this
1409        // primitive on a `Value::Null` / `Value::Bool` / `Value::Array`
1410        // handle (a malformed fetch response, an upstream default-value
1411        // fallback, a `serde_json::Value::Null` metadata slot chained
1412        // through `.and_then`) MUST get `None` back rather than a
1413        // panic or a synthesized `Some("")`. Matches the pre-lift
1414        // chain's behaviour: `Value::get` on a non-object receiver
1415        // returns `None`, `and_then` short-circuits.
1416        assert_eq!(Value::Null.get_str("any"), None);
1417        assert_eq!(Value::Bool(true).get_str("any"), None);
1418        assert_eq!(json!([1, 2, 3]).get_str("any"), None);
1419        assert_eq!(json!("scalar").get_str("any"), None);
1420    }
1421
1422    #[test]
1423    fn get_str_matches_pre_lift_hand_authored_chain_shape() {
1424        // Byte-shape parity pin: `<value>.get_str(<key>)` MUST return
1425        // the SAME `Option<&str>` the pre-lift hand-authored
1426        // `.get(<key>).and_then(|v| v.as_str())` chain produced.
1427        // Sweeps every pre-lift-reachable input corner (three
1428        // "value present" + three "value absent/malformed" arms every
1429        // status.rs / ssapply.rs callsite reached) so a regression at
1430        // the primitive that broke byte identity with the pre-lift
1431        // chain at ONE corner surfaces here rather than as a
1432        // per-slot divergence downstream.
1433        let manifest = json!({
1434            "apiVersion": "v1",
1435            "kind": "ConfigMap",
1436            "type": "Ready",
1437            "numeric": 1,
1438            "null_valued": null,
1439        });
1440        for key in [
1441            "apiVersion",
1442            "kind",
1443            "type",
1444            "numeric",
1445            "null_valued",
1446            "missing",
1447        ] {
1448            let via_primitive = manifest.get_str(key);
1449            let via_pre_lift = manifest.get(key).and_then(|v| v.as_str());
1450            assert_eq!(
1451                via_primitive, via_pre_lift,
1452                "corner `{key}` must round-trip through both shapes",
1453            );
1454        }
1455    }
1456
1457    #[test]
1458    fn get_str_composes_with_ok_or_else_at_from_json_shape() {
1459        // Downstream composition pin: the canonical caller shape at
1460        // `RenderedResourceCoords::from_json` is
1461        // `<manifest>.get_str(<key>).ok_or_else(|| anyhow!("rendered
1462        // resource missing X"))?.to_string()`. A regression that
1463        // reshaped the return form (an `&str` bare default, a
1464        // `Result<&str, _>` fallible arm) would break this
1465        // composition. Additionally sweeps the peer
1466        // `.map(String::from)` / `.map(str::to_string)` optional-slot
1467        // arm the `namespace` slot uses.
1468        let manifest = json!({ "apiVersion": "v1" });
1469        let ok_arm: String = manifest
1470            .get_str("apiVersion")
1471            .ok_or_else(|| anyhow::anyhow!("missing"))
1472            .unwrap()
1473            .to_string();
1474        assert_eq!(ok_arm, "v1");
1475        let err_arm = manifest
1476            .get_str("kind")
1477            .ok_or_else(|| anyhow::anyhow!("rendered resource missing kind"))
1478            .unwrap_err();
1479        assert_eq!(format!("{err_arm}"), "rendered resource missing kind");
1480        let opt_present: Option<String> = manifest.get_str("apiVersion").map(str::to_string);
1481        assert_eq!(opt_present.as_deref(), Some("v1"));
1482        let opt_absent: Option<String> = manifest.get_str("kind").map(String::from);
1483        assert!(opt_absent.is_none());
1484    }
1485
1486    #[test]
1487    fn get_str_return_lifetime_borrows_receiver_not_owned() {
1488        // Return-lifetime pin: the `&str` MUST borrow the receiver's
1489        // buffer rather than a fresh owned `String`. A regression that
1490        // reshaped the return to `Option<String>` (adding a
1491        // `to_string()` inside the primitive) would inflate every
1492        // callsite's allocation count and break `metadata.and_then(|m|
1493        // m.get_str("name"))`'s per-lookup zero-alloc guarantee. Bind
1494        // the invariant structurally: the borrow reaches back through
1495        // the receiver.
1496        let manifest = json!({ "apiVersion": "helm.toolkit.fluxcd.io/v2" });
1497        let s: &str = manifest.get_str("apiVersion").unwrap();
1498        let raw: &str = manifest.get("apiVersion").and_then(|v| v.as_str()).unwrap();
1499        assert!(std::ptr::eq(s.as_ptr(), raw.as_ptr()));
1500    }
1501
1502    #[test]
1503    fn get_str_axis_family_reaches_i64_and_str_through_one_trait_import() {
1504        // Axis-family pin: a caller who imports `ValueGetExt` reaches
1505        // BOTH the string axis (`get_str`) and the integer axis
1506        // (`get_i64`) through the SAME trait handle. A regression that
1507        // opened a peer `ValueGetStrExt` (or a peer trait per axis)
1508        // would break this — the caller would have to import each
1509        // trait separately and a partial import would silently miss
1510        // one axis at method-resolution time.
1511        //
1512        // Structurally: a bound `T: ValueGetExt` reaches both methods.
1513        fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>) {
1514            (t.get_i64("n"), t.get_str("s"))
1515        }
1516        let mixed = json!({ "n": 7, "s": "hello" });
1517        let (n, s) = probe(&mixed);
1518        assert_eq!(n, Some(7));
1519        assert_eq!(s, Some("hello"));
1520    }
1521
1522    // ─── ValueGetExt::get_array substrate pins ───────────────────────
1523    //
1524    // Fail-before-pass-after granularity: the `ValueGetExt::get_array`
1525    // trait method did not exist before this commit, so each test below
1526    // fails to compile pre-lift. Post-lift they collectively pin the
1527    // paired READ-shape at ONE substrate owner — a regression that
1528    // narrowed the projection to the wrong variant (accepting an
1529    // object slot via a `.values().collect()` synthesis, promoting an
1530    // absent slot to `Some(&Vec::new())`), swapped the slot lookup to
1531    // `.pointer(<key>)` (losing the direct-child semantics), or
1532    // drifted the receiver-non-object arm from `None` (silently
1533    // synthesising an empty array on a null status blob) surfaces
1534    // HERE rather than as silent operator-facing skew across the two
1535    // pre-lift consumers (`ssapply::ready_condition_value`'s
1536    // `status.conditions` walker + `probe::count_jwks_keys`'s `keys`
1537    // counter).
1538
1539    #[test]
1540    fn get_array_present_array_slot_returns_the_slice() {
1541        // Primary Ok-arm invariant: a `Value::Array` present at the
1542        // slot projects to `Some(&Vec::new())`-shaped borrow. Sweeps
1543        // the two representative shapes the pre-lift consumers walked
1544        // (a K8s `status.conditions` array of Condition objects on the
1545        // reconciler side; a JWKS `keys` array of key objects on the
1546        // probe side).
1547        let status = json!({
1548            "conditions": [
1549                { "type": "Ready", "status": "True" },
1550                { "type": "Progressing", "status": "False" },
1551            ],
1552        });
1553        let via = status.get_array("conditions").expect("Value::Array");
1554        assert_eq!(via.len(), 2);
1555        assert_eq!(via[0]["type"], "Ready");
1556
1557        let jwks = json!({
1558            "keys": [
1559                { "kty": "RSA", "kid": "1" },
1560                { "kty": "RSA", "kid": "2" },
1561                { "kty": "EC",  "kid": "3" },
1562            ],
1563        });
1564        assert_eq!(
1565            jwks.get_array("keys").map(Vec::len),
1566            Some(3),
1567            "probe count_jwks_keys composition must reach the same tail as pre-lift",
1568        );
1569    }
1570
1571    #[test]
1572    fn get_array_absent_slot_returns_none() {
1573        // Absent-slot corner: a fresh K8s status blob whose controller
1574        // has not stamped `conditions` yet (the `data.get("status")`
1575        // walker yields an object without the slot) MUST return
1576        // `None` so the caller's `let Some(...) = ... else { return
1577        // ReadyState::Unknown }` short-circuit fires. A regression that
1578        // returned `Some(&Vec::new())` on the absent corner would
1579        // silently drive the caller into an empty for-loop and skip
1580        // the fail-safe.
1581        let status = json!({});
1582        assert_eq!(status.get_array("conditions"), None);
1583        assert_eq!(status.get_array("any_missing_key"), None);
1584    }
1585
1586    #[test]
1587    fn get_array_present_but_non_array_slot_returns_none() {
1588        // Present-but-non-array corner: a `Value::String`,
1589        // `Value::Number`, `Value::Bool`, `Value::Object`, or
1590        // `Value::Null` at the slot ALL fall through to `None` —
1591        // matches the pre-lift `.and_then(|v| v.as_array())` chain
1592        // exactly. A regression that wrapped a scalar in a single-
1593        // element array under a "tolerant" refactor would silently
1594        // accept a malformed status blob whose author collapsed the
1595        // conditions array to a single scalar.
1596        let status = json!({
1597            "stringy": "ready",
1598            "numeric": 1,
1599            "boolean": true,
1600            "object": { "nested": true },
1601            "null_valued": null,
1602        });
1603        assert_eq!(status.get_array("stringy"), None);
1604        assert_eq!(status.get_array("numeric"), None);
1605        assert_eq!(status.get_array("boolean"), None);
1606        assert_eq!(status.get_array("object"), None);
1607        assert_eq!(status.get_array("null_valued"), None);
1608    }
1609
1610    #[test]
1611    fn get_array_empty_array_slot_survives_the_projection() {
1612        // Empty-array corner: a `Value::Array` with zero elements at
1613        // the slot MUST project to `Some(&Vec::new())` — matches the
1614        // pre-lift chain exactly, keeping the "authored empty" arm
1615        // distinct from the "not authored" arm upstream. The probe
1616        // consumer's `.map(|xs| xs.len() as u64).unwrap_or(0)` tail
1617        // depends on this: an authored-empty JWKS array reports 0
1618        // keys, distinct from a JWKS response missing the `keys` slot
1619        // altogether (which the caller could later choose to log
1620        // differently).
1621        let jwks = json!({ "keys": [] });
1622        let arr = jwks.get_array("keys").expect("Value::Array");
1623        assert!(arr.is_empty());
1624        assert_eq!(jwks.get_array("keys").map(Vec::len), Some(0));
1625    }
1626
1627    #[test]
1628    fn get_array_non_object_receiver_returns_none_verbatim() {
1629        // Non-object receiver corner: a caller who reached this
1630        // primitive on a `Value::Null` / `Value::Bool` / `Value::Array`
1631        // handle (a malformed fetch response, an upstream default-value
1632        // fallback, a `serde_json::Value::Null` intermediate chained
1633        // through `.and_then`) MUST get `None` back rather than a
1634        // panic or a synthesized `Some(&Vec::new())`. Matches the
1635        // pre-lift chain's behaviour: `Value::get` on a non-object
1636        // receiver returns `None`, `and_then` short-circuits.
1637        assert_eq!(Value::Null.get_array("any"), None);
1638        assert_eq!(Value::Bool(true).get_array("any"), None);
1639        assert_eq!(json!([1, 2, 3]).get_array("any"), None);
1640        assert_eq!(json!("scalar").get_array("any"), None);
1641    }
1642
1643    #[test]
1644    fn get_array_matches_pre_lift_hand_authored_chain_shape() {
1645        // Byte-shape parity pin: `<value>.get_array(<key>)` MUST return
1646        // the SAME `Option<&Vec<Value>>` the pre-lift hand-authored
1647        // `.get(<key>).and_then(|v| v.as_array())` chain produced.
1648        // Sweeps every pre-lift-reachable input corner (three
1649        // "value present" + three "value absent/malformed" arms
1650        // covering the two pre-lift consumers) so a regression at the
1651        // primitive that broke byte identity with the pre-lift chain
1652        // at ONE corner surfaces here rather than as a per-slot
1653        // divergence downstream.
1654        let manifest = json!({
1655            "conditions": [{ "type": "Ready" }],
1656            "keys": [{ "kid": "1" }, { "kid": "2" }],
1657            "empty": [],
1658            "stringy": "not-an-array",
1659            "null_valued": null,
1660        });
1661        for key in [
1662            "conditions",
1663            "keys",
1664            "empty",
1665            "stringy",
1666            "null_valued",
1667            "missing",
1668        ] {
1669            let via_primitive = manifest.get_array(key);
1670            let via_pre_lift = manifest.get(key).and_then(|v| v.as_array());
1671            assert_eq!(
1672                via_primitive, via_pre_lift,
1673                "corner `{key}` must round-trip through both shapes",
1674            );
1675        }
1676    }
1677
1678    #[test]
1679    fn get_array_composes_with_len_map_at_probe_count_jwks_keys_shape() {
1680        // Downstream composition pin: the canonical caller shape at
1681        // `probe::count_jwks_keys` is `<body_val>.get_array(<key>).
1682        // map(|xs| xs.len() as u64).unwrap_or(0)` — matches the
1683        // pre-lift `.get(<key>).cloned().and_then(|k| k.as_array().
1684        // map(|xs| xs.len() as u64)).unwrap_or(0)` chain shed of its
1685        // pre-lift `.cloned()` allocation. A regression that reshaped
1686        // the return form (an `Option<Vec<Value>>` owned, a
1687        // `Result<...>` fallible arm) would break this composition
1688        // AND reintroduce the eliminated allocation.
1689        let jwks = json!({ "keys": [{ "kid": "1" }, { "kid": "2" }, { "kid": "3" }] });
1690        let n: u64 = jwks
1691            .get_array("keys")
1692            .map(|xs| xs.len() as u64)
1693            .unwrap_or(0);
1694        assert_eq!(n, 3);
1695        // Missing slot composes to 0 through the same unwrap_or arm.
1696        let empty = json!({});
1697        let z: u64 = empty
1698            .get_array("keys")
1699            .map(|xs| xs.len() as u64)
1700            .unwrap_or(0);
1701        assert_eq!(z, 0);
1702    }
1703
1704    #[test]
1705    fn get_array_composes_with_let_else_short_circuit_at_ready_condition_shape() {
1706        // Downstream composition pin: the canonical caller shape at
1707        // `ssapply::ready_condition_value` is `let Some(conditions) =
1708        // <data>.get("status").and_then(|s| s.get_array("conditions"))
1709        // else { return ReadyState::Unknown; }` — the walker rides
1710        // the `get_array` primitive on the tail of a nested walk. A
1711        // regression that changed the return to `Option<Vec<Value>>`
1712        // owned would break the `for c in conditions` borrow-iterate
1713        // pattern downstream (each `c` borrows through the receiver).
1714        let data = json!({
1715            "status": {
1716                "conditions": [
1717                    { "type": "Ready",       "status": "True" },
1718                    { "type": "Progressing", "status": "False" },
1719                ],
1720            },
1721        });
1722        let conditions = data
1723            .get("status")
1724            .and_then(|s| s.get_array("conditions"))
1725            .expect("nested walk resolves");
1726        assert_eq!(conditions.len(), 2);
1727        // Verifies borrow-through-receiver: iterate without cloning.
1728        let types: Vec<&str> = conditions
1729            .iter()
1730            .filter_map(|c| c.get_str("type"))
1731            .collect();
1732        assert_eq!(types, vec!["Ready", "Progressing"]);
1733    }
1734
1735    #[test]
1736    fn get_array_return_lifetime_borrows_receiver_not_owned() {
1737        // Return-lifetime pin: the `&Vec<Value>` MUST borrow the
1738        // receiver's buffer rather than a fresh owned `Vec`. A
1739        // regression that reshaped the return to `Option<Vec<Value>>`
1740        // (adding a `.clone()` inside the primitive) would inflate
1741        // every callsite's allocation count and — for the
1742        // ssapply.rs caller — reintroduce a per-reconcile clone of
1743        // every K8s Condition on every DynamicObject readiness probe.
1744        // Bind the invariant structurally: the borrow reaches back
1745        // through the receiver.
1746        let manifest = json!({ "keys": [{ "kid": "1" }, { "kid": "2" }] });
1747        let via_primitive: &Vec<Value> = manifest.get_array("keys").unwrap();
1748        let via_raw: &Vec<Value> = manifest.get("keys").and_then(|v| v.as_array()).unwrap();
1749        assert!(std::ptr::eq(via_primitive.as_ptr(), via_raw.as_ptr()));
1750    }
1751
1752    #[test]
1753    fn get_array_axis_family_reaches_i64_str_and_array_through_one_trait_import() {
1754        // Axis-family pin: a caller who imports `ValueGetExt` reaches
1755        // the integer axis (`get_i64`), the string axis (`get_str`),
1756        // AND the array axis (`get_array`) through the SAME trait
1757        // handle. A regression that opened a peer `ValueGetArrayExt`
1758        // (or a peer trait per axis) would break this — the caller
1759        // would have to import each trait separately and a partial
1760        // import would silently miss one axis at method-resolution
1761        // time.
1762        //
1763        // Structurally: a bound `T: ValueGetExt` reaches all three
1764        // methods. This test extends the pre-existing
1765        // `get_str_axis_family_reaches_i64_and_str_through_one_trait_import`
1766        // sibling to cover the new axis; either drops means the
1767        // axis-family invariant no longer holds.
1768        fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>, Option<&Vec<Value>>) {
1769            (t.get_i64("n"), t.get_str("s"), t.get_array("a"))
1770        }
1771        let mixed = json!({ "n": 7, "s": "hello", "a": [1, 2, 3] });
1772        let (n, s, a) = probe(&mixed);
1773        assert_eq!(n, Some(7));
1774        assert_eq!(s, Some("hello"));
1775        assert_eq!(a.map(Vec::len), Some(3));
1776    }
1777
1778    // ─── ValueGetExt receiver-shape widening — Map impl pins ─────────
1779    //
1780    // Fail-before-pass-after granularity: `impl ValueGetExt for
1781    // Map<String, Value>` did not exist before this commit, so each
1782    // test below fails to compile pre-lift (a bare `Map<String, Value>`
1783    // receiver has no `.get_str(<key>)` inherent method — only the
1784    // upstream `.get(<key>).and_then(Value::as_str)` chain — so the
1785    // callsite fails method resolution). Post-lift they collectively
1786    // pin the widening at ONE substrate owner — a regression that
1787    // dropped the `Map` impl and re-forced every `&Map` receiver into
1788    // a `Value::Object(m.clone())` rewrap detour would surface HERE
1789    // rather than as silent per-emit skew across the 30
1790    // `Map`-receiver pre-lift consumers in `tatara-reconciler::
1791    // {patch,ssapply}` tests.
1792
1793    #[test]
1794    fn map_receiver_reaches_str_i64_and_array_axes_through_the_same_trait() {
1795        // Receiver-parity pin: an `&Map<String, Value>` handle reaches
1796        // the SAME three axes (`get_str`, `get_i64`, `get_array`) the
1797        // `&Value` receiver already exposes. A regression that
1798        // implemented only one axis on the Map arm (a copy-paste
1799        // omission at the impl block) would surface here as one of the
1800        // three assertions failing to compile / returning `None`.
1801        let obj: Map<String, Value> = json!({
1802            "s": "hello",
1803            "n": 42,
1804            "a": [1, 2, 3],
1805        })
1806        .as_object()
1807        .unwrap()
1808        .clone();
1809        assert_eq!(obj.get_str("s"), Some("hello"));
1810        assert_eq!(obj.get_i64("n"), Some(42));
1811        assert_eq!(obj.get_array("a").map(Vec::len), Some(3));
1812    }
1813
1814    #[test]
1815    fn map_receiver_get_str_matches_pre_lift_hand_authored_chain_bytewise() {
1816        // Byte-shape parity pin: `<map>.get_str(<key>)` on a
1817        // `&Map<String, Value>` MUST return the SAME `Option<&str>` the
1818        // pre-lift `.get(<key>).and_then(Value::as_str)` chain
1819        // produced. Sweeps every pre-lift-reachable corner (present
1820        // string, present non-string, absent) so a regression at the
1821        // Map impl that broke byte identity with the pre-lift chain at
1822        // ONE corner surfaces here rather than as a per-slot divergence
1823        // at every `patch::phase_status_*` / `ssapply::ownership_*` pin.
1824        let obj: Map<String, Value> = json!({
1825            "phase": "Running",
1826            "phaseSince": "2026-01-01T00:00:00Z",
1827            "message": "",
1828            "numeric": 7,
1829            "null_valued": null,
1830        })
1831        .as_object()
1832        .unwrap()
1833        .clone();
1834        for key in [
1835            "phase",
1836            "phaseSince",
1837            "message",
1838            "numeric",
1839            "null_valued",
1840            "missing",
1841        ] {
1842            let via_primitive = obj.get_str(key);
1843            let via_pre_lift = obj.get(key).and_then(Value::as_str);
1844            assert_eq!(
1845                via_primitive, via_pre_lift,
1846                "corner `{key}` on Map receiver must round-trip through both shapes",
1847            );
1848        }
1849    }
1850
1851    #[test]
1852    fn map_receiver_get_array_matches_pre_lift_hand_authored_chain_bytewise() {
1853        // Sibling to the `get_str` byte-parity pin on the array axis
1854        // — sweeps present-array / present-non-array / absent so a
1855        // regression at the Map impl's `get_array` arm surfaces here
1856        // rather than as silent drift at
1857        // `patch::finalizers_metadata_patch_wraps_list_in_two_slot_metadata_body`
1858        // and its peers whose `metadata.get("finalizers").and_then(
1859        // Value::as_array)` chain lifts through this substrate.
1860        let obj: Map<String, Value> = json!({
1861            "finalizers": ["tatara.pleme.io/process-finalizer", "other.io/finalizer"],
1862            "fluxResources": [],
1863            "stringy": "not-array",
1864        })
1865        .as_object()
1866        .unwrap()
1867        .clone();
1868        for key in ["finalizers", "fluxResources", "stringy", "missing"] {
1869            let via_primitive = obj.get_array(key);
1870            let via_pre_lift = obj.get(key).and_then(Value::as_array);
1871            assert_eq!(
1872                via_primitive, via_pre_lift,
1873                "corner `{key}` on Map receiver's array axis must round-trip through both shapes",
1874            );
1875        }
1876    }
1877
1878    #[test]
1879    fn map_receiver_get_i64_matches_pre_lift_hand_authored_chain_bytewise() {
1880        // Sibling to the `get_str` / `get_array` byte-parity pins on
1881        // the integer axis — closes the third axis of the family and
1882        // pins that a Map-receiver caller reaching this arm gets the
1883        // SAME `Option<i64>` the pre-lift chain produced.
1884        let obj: Map<String, Value> = json!({
1885            "succeeded": 2,
1886            "failed": 0,
1887            "active": 5,
1888            "stringy": "1",
1889            "null_valued": null,
1890        })
1891        .as_object()
1892        .unwrap()
1893        .clone();
1894        for key in [
1895            "succeeded",
1896            "failed",
1897            "active",
1898            "stringy",
1899            "null_valued",
1900            "missing",
1901        ] {
1902            let via_primitive = obj.get_i64(key);
1903            let via_pre_lift = obj.get(key).and_then(Value::as_i64);
1904            assert_eq!(
1905                via_primitive, via_pre_lift,
1906                "corner `{key}` on Map receiver's integer axis must round-trip through both shapes",
1907            );
1908        }
1909    }
1910
1911    #[test]
1912    fn map_receiver_get_str_matches_value_object_arm_bytewise() {
1913        // Cross-receiver coherence pin: an `&Map<String, Value>`
1914        // receiver's `.get_str(<key>)` MUST return the SAME
1915        // `Option<&str>` that walking the equivalent `Value::Object(m)`
1916        // through the pre-existing `Value` impl would. A regression
1917        // that specialised the Map arm (a slot-name-normalisation
1918        // pass, a per-fleet trim) at ONE receiver but not the other
1919        // would silently split the two receiver shapes' behaviour and
1920        // break the "widening preserves semantics" invariant.
1921        let v: Value = json!({
1922            "apiVersion": "v1",
1923            "kind": "ConfigMap",
1924            "phase": "Running",
1925        });
1926        let m: &Map<String, Value> = v.as_object().unwrap();
1927        for key in ["apiVersion", "kind", "phase", "missing"] {
1928            assert_eq!(
1929                <Map<String, Value> as ValueGetExt>::get_str(m, key),
1930                <Value as ValueGetExt>::get_str(&v, key),
1931                "receiver-shape parity: `{key}` must project identically through both impls",
1932            );
1933        }
1934    }
1935
1936    #[test]
1937    fn map_receiver_axis_family_reaches_all_three_axes_through_one_trait_import() {
1938        // Axis-family + receiver-shape pin combined: a generic
1939        // `T: ValueGetExt` bound reaches ALL THREE axes on the Map
1940        // receiver — the SAME structural invariant the pre-existing
1941        // `get_array_axis_family_reaches_...` sibling pins for the
1942        // `Value` receiver. This test walks the SAME `probe`-style
1943        // generic through the Map arm, so a regression that split the
1944        // trait into per-axis peers would break the invariant on both
1945        // receiver shapes simultaneously.
1946        fn probe<T: ValueGetExt>(t: &T) -> (Option<i64>, Option<&str>, Option<&Vec<Value>>) {
1947            (t.get_i64("n"), t.get_str("s"), t.get_array("a"))
1948        }
1949        let m: Map<String, Value> = json!({ "n": 7, "s": "hello", "a": [1, 2, 3] })
1950            .as_object()
1951            .unwrap()
1952            .clone();
1953        let (n, s, a) = probe(&m);
1954        assert_eq!(n, Some(7));
1955        assert_eq!(s, Some("hello"));
1956        assert_eq!(a.map(Vec::len), Some(3));
1957    }
1958}