Skip to main content

caixa_core/
limits.rs

1//! Lunatic-style per-process resource limits — the typed slot of
2//! `caixa.lisp` that wasm-engine consumes at component instantiation.
3//!
4//! See `theory/INSPIRATIONS.md` §III.1 for the prior-art frame: every
5//! caixa Servico runs sandboxed by default; no "trust the author".
6//!
7//! ```lisp
8//! (defcaixa
9//!   :nome   "my-service"
10//!   :versao "0.1.0"
11//!   :kind   Servico
12//!   :limits ((:memory     "64MiB")     ;; max linear memory per instance
13//!            (:fuel       1000000)     ;; max wasm-instructions per request
14//!            (:wall-clock "30s")       ;; max wall-clock per request
15//!            (:cpu        "500m"))     ;; soft cgroup CPU share (millicores)
16//!   :servicos ("servicos/my-service.computeunit.yaml"))
17//! ```
18//!
19//! Authors omit the slot for "no limits" (today's behavior). When set,
20//! wasm-engine M2 wires:
21//!
22//!   - [`LimitsSpec::memory`]      → `wasmtime::StoreLimits::memory_size`
23//!   - [`LimitsSpec::fuel`]        → `Store::set_fuel` + per-tick refill
24//!   - [`LimitsSpec::wall_clock`]  → epoch deadline cancellation
25//!   - [`LimitsSpec::cpu`]         → cgroup-v2 hint propagated via the pod spec
26
27use std::time::Duration;
28
29use serde::{Deserialize, Deserializer, Serialize, Serializer};
30use thiserror::Error;
31
32/// Hard upper bound for `:limits :memory`, in bytes — the
33/// `wasm32-wasip2` linear-memory ceiling. The canonical caixa Servico
34/// compilation target ([`theory/CAIXA-SDLC.md` §V — *Substrate /
35/// Nix*][sdlc-v]) is `wasm32-wasip2`, whose linear memory is 32-bit-
36/// addressed at a 64 KiB page size; the in-spec maximum is
37/// `2^16 pages × 2^16 bytes/page = 2^32` bytes = 4 GiB exactly.
38/// A `:limits :memory` value above this bound is structurally
39/// unreachable under wasm32: wasmtime's `Store::limiter` cannot grow
40/// past the 32-bit address space, so an authored `"8GiB"` either
41/// silently saturates at the engine's effective cap or surfaces as a
42/// `memory.grow` trap at runtime, far from the source caixa.lisp.
43///
44/// Pairs with [`LimitsError::MemoryZero`] (the zero-floor gate added
45/// by the prior typed-shape lift on this axis) to bracket the valid
46/// `:memory` set top-to-bottom: every validated value lies in
47/// `1..=LIMITS_MEMORY_WASM32_MAX_BYTES` (inclusive on both ends).
48/// Renderers ([`crate::render::servico_m2_overlay`] and the M2.5
49/// `wasm-engine` instantiator the ABSORPTION-ROADMAP names as the
50/// downstream wiring) consume the typed value with no re-validation
51/// — the value-shape gate is the structural contract.
52///
53/// Lifted as a typed `pub const` (rather than an inline literal at
54/// the [`LimitsSpec::validate`] call site) so the bound has exactly
55/// one source of truth — a future axis reaching for the same value
56/// (a future `memory64`-target opt-in raising the cap to 2^64, a
57/// wasm-engine smoke test asserting the engine's effective limit
58/// matches the typed bound, the M4 `mesh.pleme.io/v1alpha1/Caixa`
59/// CR materializer's per-`:limits :memory` admission webhook)
60/// reads from one place. Same shape every other typed bound in this
61/// crate carries ([`crate::render::DNS_1123_LABEL_MAX_LEN`],
62/// [`crate::render::GATEWAY_API_HTTP_PATH_MAX_LEN`],
63/// [`crate::render::NATS_SUBJECT_MAX_LEN`]).
64///
65/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
66pub const LIMITS_MEMORY_WASM32_MAX_BYTES: u64 = 4 * 1024 * 1024 * 1024;
67
68/// Structural floor for `:limits :memory`, in bytes — the
69/// `wasm32-wasip2` linear-memory page size. The wasm spec defines
70/// linear memory in fixed 64 KiB pages (`2^16` bytes); every typed
71/// memory cap is consumed by `wasmtime::StoreLimits::memory_size` as a
72/// per-component byte ceiling against which the engine checks every
73/// `memory.grow` request. A cap below one page (`< 65536` bytes) is
74/// structurally a "no wasm linear memory allowed" cap — instantiation
75/// of any wasm component that declares `(memory 1)` (i.e. min=1 page,
76/// the canonical default for every cdylib-shaped wasm component cargo
77/// emits) fails immediately with `memory minimum size of 1 pages
78/// exceeds memory limits`; a min=0 component traps the first
79/// `memory.grow(1)` because the next-page allocation would cross the
80/// sub-page cap. Either way the typed value the wasm-engine consumes
81/// is operationally indistinguishable from [`LimitsError::MemoryZero`]
82/// (no memory at all), but the diagnostic surfaces at engine-load
83/// time rather than at caixa-build time, far from the source
84/// caixa.lisp.
85///
86/// Pairs with [`LIMITS_MEMORY_WASM32_MAX_BYTES`] (the 4 GiB upper
87/// cap added by the prior typed-shape lift on this axis) to bracket
88/// the valid `:memory` set top-to-bottom in *operational* units, not
89/// just byte units: every validated value lies in
90/// `LIMITS_MEMORY_WASM32_PAGE_BYTES..=LIMITS_MEMORY_WASM32_MAX_BYTES`
91/// inclusive on both ends — i.e. at least one wasm32 linear memory
92/// page can be allocated, and at most the wasm32 address-space
93/// ceiling fits.
94///
95/// Lifted as a typed `pub const` (rather than an inline literal at
96/// the [`LimitsSpec::validate`] call site) so the bound has exactly
97/// one source of truth — a future axis reaching for the same value
98/// (a future `memory64`-target opt-in raising the page size, the M4
99/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits
100/// :memory` admission webhook, a wasm-engine smoke test asserting
101/// every instantiated component can fit one page within its
102/// configured cap) reads from one place. Same single-source-of-truth
103/// shape every typed bound in this crate carries
104/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
105/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
106pub const LIMITS_MEMORY_WASM32_PAGE_BYTES: u64 = 64 * 1024;
107
108/// Upper-bound ceiling on the `:limits :wall-clock` axis — every
109/// validated [`LimitsSpec::wall_clock`] past [`LimitsSpec::validate`]
110/// lies in `1ms..=LIMITS_WALL_CLOCK_MAX` (inclusive on both ends,
111/// integer-millisecond magnitudes by the canonical-form gate
112/// immediately preceding).
113///
114/// The typed field is `Option<Duration>` (the zero-floor arm
115/// [`LimitsError::WallClockZero`] already rejects `Duration::ZERO`, and
116/// the canonical-form arm [`LimitsError::WallClockNotCanonical`]
117/// already rejects sub-millisecond residue), so a programmatic struct
118/// literal (`LimitsSpec { wall_clock: Some(Duration::from_secs(86_400)),
119/// .. }` — 24h) and the equivalent author-surface form
120/// (`(:limits (:wall-clock "24h"))` — the codec emits `"<n>h"` for any
121/// integer-hour magnitude) both round-trip cleanly through serde — a
122/// structurally unbounded `Duration` ceiling. A `:wall-clock` value far
123/// above the per-process production band (Lunatic / Wasmtime documented
124/// per-call deadlines sit in the seconds-to-minutes range; Kubernetes
125/// activeDeadlineSeconds typical `≤ 3600s`; the longest per-request
126/// timeout any upstream HTTP runtime documents — Kubernetes
127/// ingress-nginx `proxy_read_timeout` — caps at the same 3600s) turns
128/// the typed per-process deadline into a nominal-only contract: the
129/// wasm-engine's epoch-deadline cancellation reaches for a `Duration`
130/// so long no realistic synchronous wasm call can hit it, the runaway-
131/// process invariant the MESH-COMPOSITION §V "no infinite blocking" CSE
132/// invariant pins at the per-Servico layer degenerates to a runtime,
133/// not build-time, contract. Pairs with the
134/// [`crate::POLICY_TIMEOUT_MAX`] cap on the sibling `:politicas :timeout`
135/// mesh-edge axis and the [`crate::POLICY_BREAKER_WINDOW_MAX`] cap on
136/// the sibling `:politicas :circuit-breaker :window` rolling-window
137/// axis — all three close the "structurally unbounded `Duration`
138/// ceiling on a typed slot" footgun the prior zero-floor-and-canonical-
139/// form-only checks left open.
140///
141/// The 1h (3600s = `3_600_000` ms) ceiling matches the largest unit
142/// the shared duration codec emits (`"<n>h"` for any integer-hour
143/// magnitude) — every value in the canonical authoring form's
144/// `<integer><unit>` grammar at or below this cap renders to a clean
145/// canonical string — and matches the two sibling typed-`Duration`
146/// caps already lifted to this surface
147/// ([`crate::POLICY_TIMEOUT_MAX`], [`crate::POLICY_BREAKER_WINDOW_MAX`]).
148/// The three typed-`Duration` axes — per-process `:limits :wall-clock`,
149/// per-edge `:politicas :timeout`, per-breaker `:politicas
150/// :circuit-breaker :window` — now share a single uniform top edge so
151/// the next typed-slot wiring (the wasm-engine M2.5 epoch-deadline
152/// cancellation hook, the future caixa-helm `pleme-computeunit` chart's
153/// `:limits` value mapping, the M4 `mesh.pleme.io/v1alpha1/Caixa` CR
154/// materializer's per-`:limits :wall-clock` admission webhook) reaches
155/// for any of the three knowing the value is in `1ms..=1h` without
156/// re-validating at the renderer layer. The cap sits above the
157/// documented per-request playbook band (Envoy / Istio / Linkerd
158/// production `≤ 60s`, AWS App Mesh / ingress-nginx typical `≤ 300s`,
159/// Kubernetes activeDeadlineSeconds typical `≤ 3600s`) and below the
160/// clearly-pathological "effectively no deadline" floor (`24h`, `7d`,
161/// `Duration::MAX`): a value the author can plausibly want for a
162/// long-running synchronous workflow, but a hard wall above which the
163/// per-process deadline is structurally a non-deadline.
164///
165/// Lifted as a typed `pub const` so the bound has exactly one source
166/// of truth — the wasm-engine M2.5 epoch-deadline wiring, a wasm-engine
167/// smoke test asserting the engine's epoch interrupt fires within the
168/// typed bound, the M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's
169/// per-`:limits :wall-clock` admission webhook all read from one place.
170/// Same shape every other typed upper bound in this crate carries
171/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`], [`crate::POLICY_TIMEOUT_MAX`],
172/// [`crate::POLICY_BREAKER_WINDOW_MAX`],
173/// [`crate::render::DNS_1123_LABEL_MAX_LEN`],
174/// [`crate::render::NATS_SUBJECT_MAX_LEN`]).
175pub const LIMITS_WALL_CLOCK_MAX: Duration = Duration::from_secs(3600);
176
177/// Upper-bound ceiling on the `:limits :cpu` axis, in Kubernetes
178/// millicores — every validated [`LimitsSpec::cpu`] past
179/// [`LimitsSpec::validate`] lies in `1..=LIMITS_CPU_MILLICORES_MAX`
180/// (inclusive on both ends).
181///
182/// The typed field is `Option<u32>` (the zero-floor arm
183/// [`LimitsError::CpuZero`] already rejects `Some(0)` — a zero cgroup
184/// share starves the process), so a programmatic struct literal
185/// (`LimitsSpec { cpu: Some(u32::MAX), .. }` — ≈ 4.3 million cores)
186/// and the equivalent author-surface form (`(:limits (:cpu
187/// "1000000m"))` — the millicore codec parses any `u32`-shaped
188/// magnitude) both round-trip cleanly through serde — a structurally
189/// unbounded `u32` ceiling. The runtime substrate consuming the value
190/// ([`crate::render::servico_m2_overlay`]'s `pleme-computeunit.limits.cpu`
191/// projection, the M2.5 `wasm-engine` instantiator the
192/// `ABSORPTION-ROADMAP` names as the downstream wiring, the future
193/// M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's admission
194/// webhook) lands the value verbatim as the K8s pod's
195/// `resources.requests.cpu`. A value far above the largest commodity
196/// node's vCPU count turns the typed slot into an unschedulable hint:
197/// the Kubernetes scheduler refuses to bind the pod to any node
198/// (insufficient `cpu` available), the Servico sits `Pending`
199/// indefinitely, and the per-process CSE invariant (every typed
200/// `:cpu` reaches a node) is a runtime, not build-time, contract —
201/// the canonical declared-but-unschedulable footgun the sibling
202/// `:limits :memory` wasm32-cap arm closes on its peer "cannot be
203/// honored" shape.
204///
205/// The `128_000` (128 cores) ceiling matches the largest commercially
206/// common non-metal cloud Kubernetes node vCPU count (AWS m7i.32xlarge
207/// / c7i.32xlarge = 128 vCPU; Azure HBv3-128rs = 128 vCPU; GCP
208/// c3-standard-128 = 128 vCPU — every major managed-Kubernetes provider
209/// tops out at 128 vCPU on its general-purpose non-metal SKUs) and sits
210/// two orders of magnitude above every realistic per-Servico
211/// production-playbook band (the canonical caixa Servico runs in the
212/// 100m–2000m band; the in-tree
213/// `limits_slot_propagates_into_values_block` smoke test pins
214/// `cpu: Some(500)` = 500m as the load-bearing example, peer to the
215/// `caixa-flux` projector's identical 500m default). A value above this
216/// cap is structurally unschedulable on any commercial managed
217/// Kubernetes node pool: GKE Standard / EKS managed / AKS default
218/// node-group SKU ladders cap at 128 vCPU per node for general-purpose
219/// instance families, so a `:cpu` request above `128_000m` cannot bind to
220/// any node the operator can provision through the standard
221/// cloud-provider control plane. The wasm32-wasip2 single-threaded
222/// execution model the canonical caixa Servico targets
223/// ([`theory/CAIXA-SDLC.md` §V][sdlc-v]) reinforces the structural
224/// argument: a single wasm component cannot saturate more than one
225/// core, so even the Lunatic-style supervised-multi-process host
226/// (`theory/INSPIRATIONS.md` §III.1) — which fans wasm processes across
227/// the host runtime's Tokio thread pool — bounds its useful CPU request
228/// to the host node's vCPU count, never higher.
229///
230/// Lifted as a typed `pub const` (rather than an inline literal at the
231/// [`LimitsSpec::validate`] call site) so the bound has exactly one
232/// source of truth — the future M4
233/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits :cpu`
234/// admission webhook, the caixa-helm `pleme-computeunit` chart's
235/// resource-request mapping, the M2.5 `wasm-engine` host-runtime
236/// thread-pool sizing hint all read from one place. Same shape every
237/// other typed upper bound in this crate carries
238/// ([`LIMITS_MEMORY_WASM32_MAX_BYTES`], [`LIMITS_WALL_CLOCK_MAX`],
239/// [`crate::POLICY_TIMEOUT_MAX`], [`crate::POLICY_BREAKER_WINDOW_MAX`],
240/// [`crate::POLICY_RATE_LIMIT_MAX`],
241/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
242///
243/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
244pub const LIMITS_CPU_MILLICORES_MAX: u32 = 128_000;
245
246/// Upper-bound ceiling on the `:limits :fuel` axis, in wasm
247/// instructions per outermost call — every validated
248/// [`LimitsSpec::fuel`] past [`LimitsSpec::validate`] lies in
249/// `1..=LIMITS_FUEL_MAX` (inclusive on both ends).
250///
251/// The typed field is `Option<u64>` (the zero-floor arm
252/// [`LimitsError::FuelZero`] already rejects `Some(0)` — wasmtime
253/// traps the first instruction at `fuel=0`), so a programmatic
254/// struct literal (`LimitsSpec { fuel: Some(u64::MAX), .. }` —
255/// ≈ 1.8 × 10¹⁹ instructions) and the equivalent author-surface
256/// form (`(:limits (:fuel 18446744073709551615))`) both
257/// round-trip cleanly through serde — a structurally unbounded
258/// `u64` ceiling. The runtime substrate consuming the value
259/// ([`crate::render::servico_m2_overlay`]'s
260/// `pleme-computeunit.limits.fuel` projection, the M2.5
261/// `wasm-engine` `Store::set_fuel` call the
262/// `ABSORPTION-ROADMAP` names as the downstream wiring, the
263/// future M4 `mesh.pleme.io/v1alpha1/Caixa` CR materializer's
264/// admission webhook) lands the value verbatim as the
265/// wasmtime store's per-call fuel budget. A value far above any
266/// reachable wasm execution count turns the typed slot into a
267/// no-op budget: the sibling [`LIMITS_WALL_CLOCK_MAX`] (1h)
268/// cap fires before the fuel counter ever drains, the per-call
269/// fuel-tracking contract degenerates to "rely on `:wall-clock`
270/// instead" enforcement, and the per-process CSE invariant
271/// (every typed `:fuel` is a meaningful budget the wasm-engine
272/// can actually consume) is a runtime, not build-time, contract
273/// on every above-cap input — the canonical declared-but-no-op
274/// footgun the sibling `:wall-clock` / `:cpu` / `:memory` cap
275/// arms close on the peer "cannot be honored" /
276/// "unschedulable hint" / "no-op budget" shapes, and the peer
277/// `:politicas :rate-limit` / `:politicas :timeout` /
278/// `:politicas :circuit-breaker :window` /
279/// `:supervisor :max-restarts` cap arms close on every other
280/// `Option<numeric>` axis on the typed Caixa surface.
281///
282/// The `1_000_000_000_000` (10¹² = 1 trillion wasm instructions)
283/// ceiling matches the operational envelope the sibling
284/// [`LIMITS_WALL_CLOCK_MAX`] cap pins: at wasmtime's documented
285/// fuel-tracked execution rate (~10⁸–10⁹ fuel-units per second
286/// on modern x86_64 / aarch64 hosts running wasmtime through
287/// Cranelift — the substrate's wasm32-wasip2 default backend per
288/// the `caixa-feira` runner), the largest realistic per-call
289/// fuel budget reachable within `LIMITS_WALL_CLOCK_MAX` (1h)
290/// sits at ~3.6 × 10¹¹–3.6 × 10¹² fuel-units. The 10¹² cap is
291/// the round-number ceiling above this operational envelope,
292/// sits six orders of magnitude above the canonical fixture
293/// (the in-tree `Caixa::template` documentation and
294/// `caixa-feira` examples carry `:fuel 1_000_000` = 10⁶,
295/// peer to wasmtime's official `Store::set_fuel(1_000_000)`
296/// example in the `wasmtime` book), and surfaces every
297/// paste-from-binary / overflow / u64-magnitude-typo footgun
298/// (`u64::MAX`, `0xFFFF_FFFF_FFFF_FFFF`, large hex literals
299/// confused for instruction-count budgets) at validate time.
300/// A value above this cap is operationally a no-op fuel
301/// counter: the wall-clock deadline ([`LIMITS_WALL_CLOCK_MAX`]
302/// = 3600s × ~10⁹ fuel/sec ≈ 3.6 × 10¹² instructions reachable)
303/// fires before the fuel counter could ever be drained,
304/// so the typed `:fuel` slot becomes a no-op budget far from
305/// the source caixa.lisp. The wasm32-wasip2 single-threaded
306/// execution model the canonical caixa Servico targets
307/// ([`theory/CAIXA-SDLC.md` §V][sdlc-v]) reinforces the
308/// structural argument: a single wasm component cannot
309/// out-execute its host's CPU clock, so even the Lunatic-style
310/// supervised-multi-process host (`theory/INSPIRATIONS.md`
311/// §III.1) bounds its useful fuel-per-call budget to a
312/// per-clock-tick magnitude, never higher.
313///
314/// Lifted as a typed `pub const` (rather than an inline literal
315/// at the [`LimitsSpec::validate`] call site) so the bound has
316/// exactly one source of truth — the future M4
317/// `mesh.pleme.io/v1alpha1/Caixa` CR materializer's per-`:limits
318/// :fuel` admission webhook, the caixa-helm `pleme-computeunit`
319/// chart's fuel-budget mapping, the M2.5 `wasm-engine` host-
320/// runtime `Store::set_fuel` propagation all read from one
321/// place. Same shape every other typed upper bound in this
322/// crate carries ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
323/// [`LIMITS_WALL_CLOCK_MAX`], [`LIMITS_CPU_MILLICORES_MAX`],
324/// [`crate::POLICY_TIMEOUT_MAX`],
325/// [`crate::POLICY_BREAKER_WINDOW_MAX`],
326/// [`crate::POLICY_RATE_LIMIT_MAX`],
327/// [`crate::SUPERVISOR_MAX_RESTARTS_MAX`],
328/// [`crate::render::DNS_1123_LABEL_MAX_LEN`]).
329///
330/// [sdlc-v]: https://github.com/pleme-io/theory/blob/main/CAIXA-SDLC.md
331pub const LIMITS_FUEL_MAX: u64 = 1_000_000_000_000;
332
333/// Per-process limits. All fields optional — `None` = unbounded for that axis.
334#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default, PartialEq, Eq)]
335#[serde(rename_all = "camelCase")]
336pub struct LimitsSpec {
337    /// Max linear memory in bytes. Authored as a byte-size string
338    /// (`"64MiB"`, `"1GiB"`, `"512KB"`). Round-trips back to the same
339    /// canonical string on serialize.
340    #[serde(
341        default,
342        skip_serializing_if = "Option::is_none",
343        serialize_with = "ser_byte_size",
344        deserialize_with = "de_byte_size"
345    )]
346    pub memory: Option<u64>,
347
348    /// Max wasm instructions per outermost call (`wasmtime` fuel).
349    /// Plain integer; `None` = unbounded.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub fuel: Option<u64>,
352
353    /// Wall-clock cap per outermost call. Authored as a duration
354    /// string (`"30s"`, `"500ms"`, `"2m"`).
355    #[serde(
356        default,
357        skip_serializing_if = "Option::is_none",
358        serialize_with = "ser_duration",
359        deserialize_with = "de_duration"
360    )]
361    pub wall_clock: Option<Duration>,
362
363    /// Soft CPU share. Authored as a Kubernetes-style millicore string
364    /// (`"500m"` for half a core, `"2"` or `"2000m"` for two cores).
365    /// Stored as millicores (u32).
366    #[serde(
367        default,
368        skip_serializing_if = "Option::is_none",
369        serialize_with = "ser_millicores",
370        deserialize_with = "de_millicores"
371    )]
372    pub cpu: Option<u32>,
373}
374
375impl LimitsSpec {
376    /// True when no axis is bounded.
377    #[must_use]
378    pub const fn is_empty(&self) -> bool {
379        self.memory().is_none()
380            && self.fuel().is_none()
381            && self.wall_clock().is_none()
382            && self.cpu().is_none()
383    }
384
385    /// Substrate-canonical per-`:limits` `:memory` Lunatic-per-process
386    /// wasm32-linear-memory byte-cap scalar accessor every consumer of
387    /// the Servico's `wasmtime::StoreLimits::memory_size` propagation
388    /// keys off — returns the author-declared `:limits :memory` typed
389    /// byte-cap verbatim as an `Option<u64>`, copied out of the typed
390    /// slot's own `Option<u64>` storage (`Option<u64>` is `Copy`, so
391    /// the accessor returns by value; no borrow of `&self` past the
392    /// call). `None` when the slot is absent (the "no memory cap
393    /// declared — engine-default applies, today the pre-M2 unbounded-
394    /// linear-memory shape" arm the module-level docstring names on
395    /// [`LimitsSpec::memory`] itself — [`LimitsSpec::is_empty`]'s
396    /// `memory().is_none()` arm reads this predicate too, so an
397    /// authored-but-unset `:limits (:memory ())` round-trips to a
398    /// `servico_m2_overlay` emission structurally identical to one
399    /// that omits the slot entirely).
400    ///
401    /// The `:limits :memory` slot carries the "per-process wasm32
402    /// linear-memory byte-cap" Lunatic-shaped sandboxing contract
403    /// (`theory/INSPIRATIONS.md` §III.1) — the typed slot's
404    /// `Option<u64>` accept-set (zero-floor rejected through
405    /// [`LimitsError::MemoryZero`], wasm32-page-floor rejected through
406    /// [`LimitsError::MemoryBelowWasm32Page`], upper-bounded by
407    /// [`LIMITS_MEMORY_WASM32_MAX_BYTES`], authored as a byte-size
408    /// string that round-trips back to the canonical form through
409    /// [`ser_byte_size`] / [`de_byte_size`]) maps onto the wasmtime
410    /// `Store::limiter`-side `memory_size` projection the wasm-engine
411    /// M2 wires and, via [`crate::render::servico_m2_overlay`], onto
412    /// the `pleme-computeunit` Helm-library-chart values sub-block's
413    /// `limits.memory` key that lands as the ComputeUnit CR's
414    /// `spec.limits.memory` field.
415    ///
416    /// Prior to this lift the `.memory` field was accessed inline at
417    /// four sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
418    /// `self.memory.is_none()` arm and three [`LimitsSpec::validate`]
419    /// arms (the numeric zero-floor arm at line 397, the wasm32-page
420    /// structural floor arm at line 427, and the wasm32 upper-cap
421    /// arm at line 449) — four open-coded field-accesses that
422    /// expressed no compile-time link back to the typed slot. A
423    /// future extension of the `:limits :memory` axis to a richer
424    /// author surface — a per-instance memory-declaration override
425    /// the operator pins through a future ComputeUnit CR-side
426    /// `spec.limits.memory` overlay, a split of the single `u64`
427    /// byte-cap into a `{min, max}` pair once wasm32's `(memory M N)`
428    /// two-arg form promotes past its current single-`max` typed
429    /// bound, a wasm64 promotion once the wasm-engine grows past the
430    /// wasm32 4 GiB structural ceiling — would have had to be
431    /// threaded through every open-coded copy in lockstep or the
432    /// emptiness predicate and the validate call would silently
433    /// disagree on which cap a given [`LimitsSpec`] resolves to.
434    /// Lifting the resolution to a typed method on the substrate
435    /// primitive means every downstream consumer of the Servico's
436    /// per-`:limits` byte-cap surface reaches for exactly one typed
437    /// dispatch — the resolver's accept-set migrates as a unit on any
438    /// future axis addition.
439    ///
440    /// First `Option<Copy-T>`-return accessor on the M2 slot family
441    /// (peer of the sibling per-`:politicas` [`crate::MeshPolicy::mtls_required`]
442    /// c0110f1 `Option<bool>` accessor, per-`:politicas`
443    /// [`crate::MeshPolicy::retries`] bdfb399 `Option<u32>` accessor,
444    /// and per-`:politicas` [`crate::MeshPolicy::timeout`] 7073d0f
445    /// `Option<Duration>` accessor on the M3 mesh-slot family — same
446    /// "one typed dispatch on the substrate primitive, thin
447    /// projections at each consumer" discipline extended onto the
448    /// peer per-`:limits` typed-`u64` optional-scalar axis; opens the
449    /// "optional per-slot Copy-T scalar" projection pattern the
450    /// sibling per-`:limits` `:fuel` (Option<u64>) / `:wall-clock`
451    /// (Option<Duration>) / `:cpu` (Option<u32>) future lifts fold
452    /// on). Named `memory()` to match the storage field's name; the
453    /// accessor's identity maps onto the canonical Lunatic-shaped
454    /// `theory/INSPIRATIONS.md` §III.1 vocabulary the slot's docstring
455    /// already carries.
456    #[must_use]
457    pub const fn memory(&self) -> Option<u64> {
458        self.memory
459    }
460
461    /// Substrate-canonical per-`:limits` `:fuel` wasmtime-per-call
462    /// wasm-instruction budget scalar accessor every consumer of the
463    /// Servico's `wasmtime::Store::set_fuel` propagation keys off —
464    /// returns the author-declared `:limits :fuel` typed
465    /// wasm-instruction budget verbatim as an `Option<u64>`, copied
466    /// out of the typed slot's own `Option<u64>` storage
467    /// (`Option<u64>` is `Copy`, so the accessor returns by value; no
468    /// borrow of `&self` past the call). `None` when the slot is
469    /// absent (the "no fuel budget declared — engine-default applies,
470    /// today the pre-M2 unbounded-fuel-counter shape" arm the
471    /// module-level docstring names on [`LimitsSpec::fuel`] itself —
472    /// [`LimitsSpec::is_empty`]'s `fuel().is_none()` arm reads this
473    /// predicate too, so an authored-but-unset `:limits (:fuel ())`
474    /// round-trips to a `servico_m2_overlay` emission structurally
475    /// identical to one that omits the slot entirely).
476    ///
477    /// The `:limits :fuel` slot carries the "per-call wasm-instruction
478    /// budget" wasmtime-shaped sandboxing contract
479    /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
480    /// wasm-`Store`-per-process fuel accounting, translated onto
481    /// pleme-io's typed `:limits` slot) — the typed slot's
482    /// `Option<u64>` accept-set (zero-floor rejected through
483    /// [`LimitsError::FuelZero`] because wasmtime traps the first
484    /// instruction at `fuel=0`, upper-bounded by [`LIMITS_FUEL_MAX`]
485    /// (10¹² wasm instructions — the operationally-reachable
486    /// per-call budget within the sibling [`LIMITS_WALL_CLOCK_MAX`]
487    /// 1h ceiling)) maps onto the wasmtime `Store::set_fuel` call
488    /// the M2.5 wasm-engine wires per outermost call and, via
489    /// [`crate::render::servico_m2_overlay`], onto the
490    /// `pleme-computeunit` Helm-library-chart values sub-block's
491    /// `limits.fuel` key that lands as the `ComputeUnit` CR's
492    /// `spec.limits.fuel` field.
493    ///
494    /// Prior to this lift the `.fuel` field was accessed inline at
495    /// two sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
496    /// `self.fuel.is_none()` arm and [`LimitsSpec::validate`]'s
497    /// `if let Some(f) = self.fuel { … }` zero-floor + upper-cap
498    /// bracket arm — two open-coded field-accesses that expressed no
499    /// compile-time link back to the typed slot. A future extension
500    /// of the `:limits :fuel` axis to a richer author surface — a
501    /// per-instance `ComputeUnit` CR-side `spec.limits.fuel` overlay
502    /// the operator pins per-cluster, a wasm-instruction-count →
503    /// wasmtime-fuel-unit rescale once the fuel-tracking backend
504    /// switches from Cranelift's implicit 1:1 count to a
505    /// per-opcode-weighted budget, a split of the single
506    /// per-outermost-call `u64` budget into a `{per_call, per_second}`
507    /// pair once the wasm-engine grows a sustained-throughput cap —
508    /// would have had to be threaded through every open-coded copy in
509    /// lockstep or the emptiness predicate and the validate call
510    /// would silently disagree on which fuel budget a given
511    /// [`LimitsSpec`] resolves to. Lifting the resolution to a typed
512    /// method on the substrate primitive means every downstream
513    /// consumer of the Servico's per-`:limits` fuel-budget surface
514    /// reaches for exactly one typed dispatch — the resolver's
515    /// accept-set migrates as a unit on any future axis addition.
516    ///
517    /// Second `Option<Copy-T>`-return accessor on the M2 slot family
518    /// (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
519    /// (620c067) `Option<u64>` accessor — same typed-`u64`
520    /// optional-scalar shape, extended to the peer per-`:limits`
521    /// wasm-instruction-budget axis; sibling to
522    /// [`crate::MeshPolicy::mtls_required`] (c0110f1) / [`crate::MeshPolicy::retries`]
523    /// (bdfb399) / [`crate::MeshPolicy::timeout`] (7073d0f) on the
524    /// closed M3 mesh-slot `Option<Copy-T>` accessor family). The
525    /// pair `(memory(), fuel())` jointly projects the two `Option<u64>`
526    /// axes every M2 `:limits` consumer that fans on
527    /// wasm-linear-memory-cap + wasm-fuel-budget keys off. Two of the
528    /// four `:limits` axes now route through a typed dispatch on the
529    /// substrate primitive; the two remaining (`wall_clock:
530    /// Option<Duration>`, `cpu: Option<u32>`) fold on the same
531    /// one-line accessor + is_empty-arm-route + validate-arm-route +
532    /// three-test pattern. Named `fuel()` to match the storage field's
533    /// name; the accessor's identity maps onto the canonical
534    /// wasmtime-`Store::set_fuel`-shaped vocabulary the slot's
535    /// docstring already carries.
536    #[must_use]
537    pub const fn fuel(&self) -> Option<u64> {
538        self.fuel
539    }
540
541    /// Substrate-canonical per-`:limits` `:wall-clock` wasmtime-per-call
542    /// wall-clock deadline scalar accessor every consumer of the
543    /// Servico's `wasmtime::Store::epoch_deadline_*` / `wasi:clocks`
544    /// propagation keys off — returns the author-declared `:limits
545    /// :wall-clock` typed `Duration` verbatim as an `Option<Duration>`,
546    /// copied out of the typed slot's own `Option<Duration>` storage
547    /// (`Duration` is `Copy`, so `Option<Duration>` is `Copy` and the
548    /// accessor returns by value; no borrow of `&self` past the call).
549    /// `None` when the slot is absent (the "no wall-clock deadline
550    /// declared — engine-default applies, today the pre-M2
551    /// unbounded-wall-clock shape" arm the module-level docstring names
552    /// on [`LimitsSpec::wall_clock`] itself — [`LimitsSpec::is_empty`]'s
553    /// `wall_clock().is_none()` arm reads this predicate too, so an
554    /// authored-but-unset `:limits (:wall-clock ())` round-trips to a
555    /// `servico_m2_overlay` emission structurally identical to one that
556    /// omits the slot entirely).
557    ///
558    /// The `:limits :wall-clock` slot carries the "per-outermost-call
559    /// wall-clock deadline" wasmtime-shaped sandboxing contract
560    /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
561    /// wasm-`Store`-per-process epoch-deadline accounting, translated
562    /// onto pleme-io's typed `:limits` slot) — the typed slot's
563    /// `Option<Duration>` accept-set (zero-floor rejected through
564    /// [`LimitsError::WallClockZero`] because a zero deadline traps the
565    /// first instruction; integer-millisecond granularity enforced
566    /// through [`LimitsError::WallClockNotCanonical`] because the
567    /// duration codec's canonical form emits `"1500ms"` not `"1.5s"`
568    /// and the operator's wall-clock scheduler quantizes at
569    /// milliseconds; upper-bounded by [`LIMITS_WALL_CLOCK_MAX`] (1h —
570    /// the coarsest per-call deadline any operationally-reachable
571    /// Servico can honor without spanning multiple scheduler epochs))
572    /// maps onto the wasmtime `Store::epoch_deadline_*` call the M2.5
573    /// wasm-engine wires per outermost call and, via
574    /// [`crate::render::servico_m2_overlay`], onto the
575    /// `pleme-computeunit` Helm-library-chart values sub-block's
576    /// `limits.wallClock` key that lands as the `ComputeUnit` CR's
577    /// `spec.limits.wallClock` field.
578    ///
579    /// Prior to this lift the `.wall_clock` field was accessed inline at
580    /// two sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
581    /// `self.wall_clock.is_none()` arm and [`LimitsSpec::validate`]'s
582    /// `if let Some(w) = self.wall_clock { … }` zero-floor +
583    /// canonical-form + upper-cap bracket arm — two open-coded
584    /// field-accesses that expressed no compile-time link back to the
585    /// typed slot. A future extension of the `:limits :wall-clock` axis
586    /// to a richer author surface — a per-instance `ComputeUnit`
587    /// CR-side `spec.limits.wallClock` overlay the operator pins
588    /// per-cluster, a wall-clock-vs-monotonic-clock discriminator once
589    /// the wasm-engine grows a `:limits (:wall-clock (:kind monotonic
590    /// …))` axis, a split of the single per-outermost-call `Duration`
591    /// budget into a `{deadline, warn_at}` pair once the wasm-engine
592    /// grows a soft-deadline warning surface — would have had to be
593    /// threaded through every open-coded copy in lockstep or the
594    /// emptiness predicate and the validate call would silently
595    /// disagree on which deadline a given [`LimitsSpec`] resolves to.
596    /// Lifting the resolution to a typed method on the substrate
597    /// primitive means every downstream consumer of the Servico's
598    /// per-`:limits` wall-clock-deadline surface reaches for exactly
599    /// one typed dispatch — the resolver's accept-set migrates as a
600    /// unit on any future axis addition.
601    ///
602    /// Third `Option<Copy-T>`-return accessor on the M2 slot family
603    /// (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
604    /// (620c067) `Option<u64>` accessor and per-`:limits`
605    /// [`LimitsSpec::fuel`] (795dee7) `Option<u64>` accessor — same
606    /// typed-optional-scalar shape extended to the peer per-`:limits`
607    /// wall-clock-deadline axis; sibling to [`crate::MeshPolicy::timeout`]
608    /// (7073d0f) on the closed M3 mesh-slot `Option<Duration>` accessor
609    /// axis — same typed-`Duration` shape extended from the M3
610    /// per-call-timeout to the M2 per-outermost-call deadline). The
611    /// triple `(memory(), fuel(), wall_clock())` jointly projects three
612    /// of the four `Option<Copy-T>` axes every M2 `:limits` consumer
613    /// that fans on wasm-linear-memory-cap + wasm-fuel-budget +
614    /// wall-clock-deadline keys off. Three of the four `:limits` axes
615    /// now route through a typed dispatch on the substrate primitive;
616    /// the one remaining (`cpu: Option<u32>`) folds on the same
617    /// one-line accessor + is_empty-arm-route + validate-arm-route +
618    /// three-test pattern in the next run, closing the M2 `:limits`
619    /// slot family's `Option<Copy-T>` accessor axis. Named `wall_clock()`
620    /// to match the storage field's name; the accessor's identity maps
621    /// onto the canonical wasmtime-`Store::epoch_deadline_*`-shaped
622    /// vocabulary the slot's docstring already carries.
623    #[must_use]
624    pub const fn wall_clock(&self) -> Option<Duration> {
625        self.wall_clock
626    }
627
628    /// Substrate-canonical per-`:limits` `:cpu` Kubernetes-millicore
629    /// soft cgroup-share scalar accessor every consumer of the Servico's
630    /// pod-spec `resources.requests.cpu` propagation keys off — returns
631    /// the author-declared `:limits :cpu` typed millicore magnitude
632    /// verbatim as an `Option<u32>`, copied out of the typed slot's own
633    /// `Option<u32>` storage (`Option<u32>` is `Copy`, so the accessor
634    /// returns by value; no borrow of `&self` past the call). `None`
635    /// when the slot is absent (the "no cpu share declared —
636    /// scheduler-default applies, today the pre-M2 unbounded-cpu-share
637    /// shape" arm the module-level docstring names on
638    /// [`LimitsSpec::cpu`] itself — [`LimitsSpec::is_empty`]'s
639    /// `cpu().is_none()` arm reads this predicate too, so an
640    /// authored-but-unset `:limits (:cpu ())` round-trips to a
641    /// `servico_m2_overlay` emission structurally identical to one that
642    /// omits the slot entirely).
643    ///
644    /// The `:limits :cpu` slot carries the "per-process soft cgroup-v2
645    /// CPU share" Kubernetes-scheduler-shaped sandboxing hint
646    /// (`theory/INSPIRATIONS.md` §III.1 — Lunatic's supervised
647    /// wasm-`Store`-per-process host-runtime CPU accounting, translated
648    /// onto pleme-io's typed `:limits` slot as a scheduler-facing
649    /// millicore request the pod's kubelet propagates to the container's
650    /// cgroup) — the typed slot's `Option<u32>` accept-set (zero-floor
651    /// rejected through [`LimitsError::CpuZero`] because a zero cgroup
652    /// share starves the process; upper-bounded by
653    /// [`LIMITS_CPU_MILLICORES_MAX`] (128 cores — the largest commercially-
654    /// common non-metal cloud Kubernetes node vCPU count on managed GKE
655    /// / EKS / AKS general-purpose SKUs)) maps onto the K8s pod spec's
656    /// `spec.containers[].resources.requests.cpu` field the
657    /// M2.5 `wasm-engine` host-runtime lands on the `ComputeUnit` CR-side
658    /// pod template and, via [`crate::render::servico_m2_overlay`], onto
659    /// the `pleme-computeunit` Helm-library-chart values sub-block's
660    /// `limits.cpu` key that lands as the `ComputeUnit` CR's
661    /// `spec.limits.cpu` field.
662    ///
663    /// Prior to this lift the `.cpu` field was accessed inline at two
664    /// sites inside `impl LimitsSpec` — [`LimitsSpec::is_empty`]'s
665    /// `self.cpu.is_none()` arm and [`LimitsSpec::validate`]'s
666    /// `if let Some(m) = self.cpu { … }` zero-floor + upper-cap bracket
667    /// arm — two open-coded field-accesses that expressed no
668    /// compile-time link back to the typed slot. A future extension of
669    /// the `:limits :cpu` axis to a richer author surface — a
670    /// per-instance `ComputeUnit` CR-side `spec.limits.cpu` overlay the
671    /// operator pins per-cluster, a split of the single `u32` millicore
672    /// request into a `{request, limit}` pair once the pod spec's
673    /// `resources.requests.cpu` / `resources.limits.cpu` distinction
674    /// promotes past its current single-request author surface, a
675    /// millicore → cgroup-v2 `cpu.weight` rescale once the operator's
676    /// scheduler-facing translation lands past its current kubelet
677    /// passthrough — would have had to be threaded through every
678    /// open-coded copy in lockstep or the emptiness predicate and the
679    /// validate call would silently disagree on which cgroup share a
680    /// given [`LimitsSpec`] resolves to. Lifting the resolution to a
681    /// typed method on the substrate primitive means every downstream
682    /// consumer of the Servico's per-`:limits` cpu-share surface reaches
683    /// for exactly one typed dispatch — the resolver's accept-set
684    /// migrates as a unit on any future axis addition.
685    ///
686    /// Fourth and final `Option<Copy-T>`-return accessor on the M2 slot
687    /// family (peer of the sibling per-`:limits` [`LimitsSpec::memory`]
688    /// (620c067) `Option<u64>` accessor, per-`:limits`
689    /// [`LimitsSpec::fuel`] (795dee7) `Option<u64>` accessor, and
690    /// per-`:limits` [`LimitsSpec::wall_clock`] (8cb717b)
691    /// `Option<Duration>` accessor — same typed-optional-scalar shape
692    /// extended to the peer per-`:limits` cgroup-cpu-share axis; sibling
693    /// to [`crate::MeshPolicy::mtls_required`] (c0110f1) /
694    /// [`crate::MeshPolicy::retries`] (bdfb399) /
695    /// [`crate::MeshPolicy::timeout`] (7073d0f) on the closed M3
696    /// mesh-slot `Option<Copy-T>` accessor family). The four-tuple
697    /// `(memory(), fuel(), wall_clock(), cpu())` jointly projects every
698    /// `Option<Copy-T>` axis on the M2 `:limits` slot every consumer
699    /// that fans on wasm-linear-memory-cap + wasm-fuel-budget +
700    /// wall-clock-deadline + cgroup-cpu-share keys off — closes the M2
701    /// `:limits` slot family's `Option<Copy-T>` accessor axis (the
702    /// last unlifted `:limits` field-access site on the M2 slot family;
703    /// every axis now routes through a typed dispatch on the substrate
704    /// primitive, with no open-coded field access anywhere on the impl).
705    /// Named `cpu()` to match the storage field's name; the accessor's
706    /// identity maps onto the canonical Kubernetes-`resources.requests.cpu`-
707    /// shaped vocabulary the slot's docstring already carries.
708    #[must_use]
709    pub const fn cpu(&self) -> Option<u32> {
710        self.cpu
711    }
712
713    /// Reject operationally-meaningless zero values on every declared
714    /// axis. Each axis remains optional — omitting a field expresses
715    /// "no bound on this axis"; the bug being closed is *carrying* a
716    /// zero value, which the wasm-engine consumes as "trap the first
717    /// instruction" / "instantiation refused" / "immediate timeout"
718    /// rather than the author's intended "an unspecified bound".
719    ///
720    /// Mirrors the discipline applied to `:politicas` axes in
721    /// `AplicacaoSpec::validate` and to `SupervisorSpec::max_restarts`
722    /// — every typed value carried by a slot is either absent or
723    /// meaningfully non-zero.
724    pub fn validate(&self) -> Result<(), LimitsError> {
725        // Route the `:memory` axis's four value-shape gates
726        // (zero-floor → wasm32-page-floor → wasm32-address-cap →
727        // page-multiple) through the substrate helper
728        // [`crate::render::require_positive_quantum_multiple_bounded_u64`]
729        // rather than four sequential inline
730        // `if let Some(m) = self.memory()` guards each restating one
731        // arm. Brings the `:memory` axis onto the same "one substrate
732        // helper per typed axis" discipline the peer `:fuel` (routed
733        // through [`crate::render::require_positive_bounded_u64`]),
734        // `:wall-clock` (through
735        // [`crate::render::require_positive_canonical_bounded_duration`]),
736        // and `:cpu` (through
737        // [`crate::render::require_positive_bounded_u32`]) axes
738        // already carry — every `LimitsSpec::validate` axis is now
739        // exactly one typed-helper dispatch, with the four-arm
740        // ordering (zero → below-quantum → cap → not-multiple)
741        // promoted from a per-site convention four inline blocks
742        // re-derived by hand to a structural contract on the
743        // substrate primitive. Byte-equal today: the helper fires the
744        // same four arms in the same canonical order at the same
745        // boundary values, threading the offending byte count into
746        // the same `MemoryBelowWasm32Page` / `MemoryExceedsWasm32Cap`
747        // / `MemoryNotPageMultiple` discriminator fields the four
748        // pre-lift inline arms already carried, so every existing
749        // per-arm test in this module continues to pin the same
750        // shape unchanged. Pinned end-to-end by
751        // `validate_memory_axis_routes_through_quantum_multiple_bounded_helper`.
752        if let Some(m) = self.memory() {
753            crate::render::require_positive_quantum_multiple_bounded_u64(
754                m,
755                LIMITS_MEMORY_WASM32_PAGE_BYTES,
756                LIMITS_MEMORY_WASM32_MAX_BYTES,
757                || LimitsError::MemoryZero,
758                |bytes| LimitsError::MemoryBelowWasm32Page { bytes },
759                |bytes| LimitsError::MemoryExceedsWasm32Cap { bytes },
760                |bytes| LimitsError::MemoryNotPageMultiple { bytes },
761            )?;
762        }
763        // Zero-floor + upper-cap bracket on the typed `:fuel` axis. See
764        // [`crate::render::require_positive_bounded_u64`] for the
765        // ordering discipline (zero-floor arm strictly precedes cap arm
766        // so `Some(0)` surfaces the self-locating `FuelZero` diagnostic
767        // with its omit-axis remediation directly named, not the
768        // misleading `0 > LIMITS_FUEL_MAX == false` cap-arm miss).
769        // Until this bracket landed the `Option<u64>` slot accepted any
770        // value past zero (the parser's only upper bound was `u64::MAX`),
771        // so `(:fuel 18446744073709551615)` round-tripped cleanly
772        // through serde and the per-process CSE invariant (no value the
773        // wasm-engine's fuel counter can't honor as a meaningful budget
774        // before the sibling `:wall-clock` deadline fires) was a
775        // runtime, not build-time, contract on every above-cap input
776        // — the canonical declared-but-no-op footgun the sibling
777        // [`LimitsError::MemoryExceedsWasm32Cap`] /
778        // [`LimitsError::WallClockExceedsCap`] /
779        // [`LimitsError::CpuExceedsCap`] arms close on the peer
780        // "cannot be honored" / "unschedulable hint" /
781        // "nominal-only deadline" shapes, the peer
782        // [`crate::AplicacaoError::PolicyTimeoutExceedsCap`] /
783        // [`crate::AplicacaoError::PolicyBreakerWindowExceedsCap`] /
784        // [`crate::AplicacaoError::PolicyRateLimitExceedsCap`] arms
785        // close on the no-op-deadline / lifetime-counter / no-op-limiter
786        // shapes, and the
787        // [`crate::SupervisorError::MaxRestartsExceedsCap`] arm closes
788        // on the no-op-supervisor shape. The four `:limits` axes are
789        // now uniformly bracketed top and bottom (`:memory` in
790        // `LIMITS_MEMORY_WASM32_PAGE_BYTES..=LIMITS_MEMORY_WASM32_MAX_BYTES`,
791        // `:fuel` in `1..=LIMITS_FUEL_MAX`, `:wall-clock` in
792        // `1ms..=LIMITS_WALL_CLOCK_MAX`, `:cpu` in
793        // `1..=LIMITS_CPU_MILLICORES_MAX`).
794        if let Some(f) = self.fuel() {
795            crate::render::require_positive_bounded_u64(
796                f,
797                LIMITS_FUEL_MAX,
798                || LimitsError::FuelZero,
799                |fuel| LimitsError::FuelExceedsCap { fuel },
800            )?;
801        }
802        if let Some(w) = self.wall_clock() {
803            // Zero-floor + integer-millisecond canonical-form +
804            // upper-cap bracket on the typed `:wall-clock` axis. See
805            // [`crate::render::require_positive_canonical_bounded_duration`]
806            // for the full three-arm ordering discipline (zero-floor
807            // strictly precedes canonical-form so `Duration::ZERO`
808            // surfaces the self-locating `WallClockZero` diagnostic;
809            // canonical-form strictly precedes the cap arm so a
810            // sub-millisecond above-cap value surfaces the more
811            // fundamental round-trip-shape diagnostic first) and the
812            // three peer typed-`Duration` sites that share this
813            // canonical bracket ([`crate::MeshPolicy::timeout`],
814            // [`crate::CircuitBreaker::window`],
815            // [`crate::SupervisorSpec::restart_window`]). Every
816            // validated value lies in `1ms..=LIMITS_WALL_CLOCK_MAX`
817            // (1ms..=1h), integer-millisecond granularity.
818            crate::render::require_positive_canonical_bounded_duration(
819                w,
820                LIMITS_WALL_CLOCK_MAX,
821                || LimitsError::WallClockZero,
822                |wall_clock| LimitsError::WallClockNotCanonical { wall_clock },
823                |wall_clock| LimitsError::WallClockExceedsCap { wall_clock },
824            )?;
825        }
826        // Zero-floor + upper-cap bracket on the typed `:cpu` axis. See
827        // [`crate::render::require_positive_bounded_u32`] for the
828        // ordering discipline (zero-floor arm strictly precedes cap arm
829        // so `Some(0)` surfaces the self-locating `CpuZero` diagnostic
830        // with its omit-axis remediation directly named, not the
831        // misleading `0 > LIMITS_CPU_MILLICORES_MAX == false` cap-arm
832        // miss). The bracket set is `1..=LIMITS_CPU_MILLICORES_MAX`
833        // (128 cores = 128_000 millicores — the largest commercially-
834        // common non-metal cloud Kubernetes node vCPU count). Until
835        // this bracket landed the millicore codec accepted any
836        // `Option<u32>` past zero (the prior numeric-zero arm's only
837        // floor), so `(:cpu "1000000m")` (1000 cores) round-tripped
838        // cleanly through serde and the per-axis CSE invariant (no
839        // value the Kubernetes scheduler can't honor) was a runtime,
840        // not build-time, contract on every above-cap input: the
841        // `pleme-computeunit` chart's `resources.requests.cpu` landed
842        // verbatim, the pod sat `Pending` indefinitely with a `0/N
843        // nodes are available: N Insufficient cpu` event, and the
844        // typed `:cpu` slot became an unschedulable hint far from the
845        // source caixa.lisp. Closes the same gap the wasm32-wasip2
846        // upper ceiling closes on the `:memory` axis — the typed `:cpu`
847        // axis is now operationally bracketed. Peer with every sibling
848        // cap arm on this surface ([`LimitsError::MemoryExceedsWasm32Cap`],
849        // [`LimitsError::WallClockExceedsCap`],
850        // [`crate::AplicacaoError::PolicyTimeoutExceedsCap`],
851        // [`crate::AplicacaoError::PolicyRetriesExceedsCap`],
852        // [`crate::AplicacaoError::PolicyBreakerMaxFailuresExceedsCap`],
853        // [`crate::AplicacaoError::PolicyBreakerWindowExceedsCap`],
854        // [`crate::AplicacaoError::PolicyRateLimitExceedsCap`],
855        // [`crate::SupervisorError::MaxRestartsExceedsCap`]).
856        if let Some(m) = self.cpu() {
857            crate::render::require_positive_bounded_u32(
858                m,
859                LIMITS_CPU_MILLICORES_MAX,
860                || LimitsError::CpuZero,
861                |millicores| LimitsError::CpuExceedsCap { millicores },
862            )?;
863        }
864        Ok(())
865    }
866}
867
868#[derive(Debug, Error, PartialEq, Eq)]
869pub enum LimitsError {
870    #[error("byte-size: missing magnitude in {0:?}")]
871    EmptyByteSize(String),
872    #[error("byte-size: unknown unit {unit:?} (expected one of B, KB, MB, GB, KiB, MiB, GiB)")]
873    UnknownByteUnit { unit: String },
874    #[error("byte-size: failed to parse magnitude {0:?}")]
875    BadByteMagnitude(String),
876    #[error(
877        "byte-size: magnitude {value:?} is not a non-negative integer — the canonical \
878         authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"1024\"`, \
879         `\"64MiB\"`, `\"1GiB\"`) with no decimal point and no leading `+` sign. A \
880         fractional / decimal-shaped magnitude (`\"1.5KiB\"`, `\"1.0MiB\"`, `\"0.5GiB\"`, \
881         `\"+1024\"`) round-trips through `render_byte_size` to a *different* canonical \
882         form (`\"1536\"`, `\"1MiB\"`, `\"512MiB\"`, `\"1KiB\"`) on first serialize — \
883         breaking the THEORY.md §V.2.7 render-determinism contract every typed slot \
884         carries. Pick an integer magnitude in the unit that divides cleanly (write \
885         `\"1536\"` instead of `\"1.5KiB\"`; `\"512MiB\"` instead of `\"0.5GiB\"`)"
886    )]
887    NonIntegerByteMagnitude { value: String },
888    #[error(
889        "byte-size: magnitude {value:?} has a non-canonical leading zero — the canonical \
890         authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
891         `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no leading-zero padding on the magnitude. \
892         A leading-zero magnitude (`\"064MiB\"`, `\"01024\"`, `\"00KiB\"`, `\"0500MB\"`) round-trips \
893         through `render_byte_size` to a *different* canonical form (`\"64MiB\"`, `\"1KiB\"`, \
894         `\"0\"`, `\"500MB\"`) on first serialize — breaking the THEORY.md Part V \
895         render-determinism contract every typed slot carries. Strip the leading zeros \
896         (write `\"64MiB\"` instead of `\"064MiB\"`)"
897    )]
898    LeadingZeroByteMagnitude { value: String },
899    #[error(
900        "byte-size: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
901         authoring form for `:limits :memory` is `<integer><unit>` (e.g. `\"64MiB\"`, \
902         `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) with no whitespace bytes anywhere. A \
903         whitespace-carrying shape (`\" 64MiB\"`, `\"64MiB \"`, `\"64 MiB\"`, `\"\\t64MiB\"`, \
904         `\"64MiB\\n\"`) round-trips through `render_byte_size` to a *different* canonical \
905         form (`\"64MiB\"`) on first serialize — breaking the THEORY.md Part V \
906         render-determinism contract every typed slot carries. Strip every whitespace byte \
907         (write `\"64MiB\"` verbatim)"
908    )]
909    WhitespaceInByteSize { value: String, byte: u8 },
910    #[error(
911        "byte-size: value {value:?} contains a non-ASCII Unicode whitespace character \
912         {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :memory` \
913         is `<integer><unit>` (e.g. `\"64MiB\"`, `\"1GiB\"`, `\"512KiB\"`, `\"1024\"`) \
914         with no whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
915         carrying shape (`\"\\u{{00A0}}64MiB\"` — paste-from-typography NBSP prefix; \
916         `\"64MiB\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
917         `\"64\\u{{2003}}MiB\"` — paste-from-typography EM-SPACE between magnitude and \
918         unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
919         its bytes match the ASCII whitespace set) but `str::trim` (which uses \
920         `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
921         the ASCII byte set) silently strips it at parse entry, and the value round-trips \
922         through `render_byte_size` to a *different* canonical form (`\"64MiB\"`) on \
923         first serialize — breaking the THEORY.md Part V render-determinism contract \
924         every typed slot carries. Strip every non-ASCII whitespace character (write \
925         `\"64MiB\"` verbatim with only ASCII bytes)"
926    )]
927    NonAsciiWhitespaceInByteSize {
928        value: String,
929        ch: char,
930        codepoint: u32,
931    },
932    #[error("duration: missing magnitude in {0:?}")]
933    EmptyDuration(String),
934    #[error("duration: unknown unit {unit:?} (expected one of ms, s, m, h)")]
935    UnknownDurationUnit { unit: String },
936    #[error("duration: failed to parse magnitude {0:?}")]
937    BadDurationMagnitude(String),
938    #[error(
939        "duration: magnitude {value:?} is not a non-negative integer — the canonical \
940         authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
941         `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no decimal point and no leading `+` sign. A \
942         fractional / decimal-shaped magnitude (`\"1.5s\"`, `\"1.0s\"`, `\"0.5m\"`, \
943         `\"+30s\"`, `\"-30s\"`) round-trips through `render_duration` to a *different* \
944         canonical form (`\"1500ms\"`, `\"1s\"`, `\"30s\"`, `\"30s\"`) on first serialize \
945         — breaking the THEORY.md Part V render-determinism contract every typed slot \
946         carries. Pick an integer magnitude in the unit that divides cleanly (write \
947         `\"1500ms\"` instead of `\"1.5s\"`; `\"30s\"` instead of `\"0.5m\"`)"
948    )]
949    NonIntegerDurationMagnitude { value: String },
950    #[error(
951        "duration: magnitude {value:?} has a non-canonical leading zero — the canonical \
952         authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
953         `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no leading-zero padding on the magnitude. \
954         A leading-zero magnitude (`\"030s\"`, `\"00s\"`, `\"01h\"`, `\"0500ms\"`) round-trips \
955         through `render_duration` to a *different* canonical form (`\"30s\"`, `\"0s\"`, \
956         `\"1h\"`, `\"500ms\"`) on first serialize — breaking the THEORY.md Part V \
957         render-determinism contract every typed slot carries. Strip the leading zeros \
958         (write `\"30s\"` instead of `\"030s\"`)"
959    )]
960    LeadingZeroDurationMagnitude { value: String },
961    #[error(
962        "duration: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
963         authoring form for `:limits :wall-clock` is `<integer><unit>` (e.g. `\"30s\"`, \
964         `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no whitespace bytes anywhere. A \
965         whitespace-carrying shape (`\" 30s\"`, `\"30s \"`, `\"30 s\"`, `\"\\t30s\"`, \
966         `\"30s\\n\"`) round-trips through `render_duration` to a *different* canonical form \
967         (`\"30s\"`) on first serialize — breaking the THEORY.md Part V render-determinism \
968         contract every typed slot carries. Strip every whitespace byte (write `\"30s\"` \
969         verbatim)"
970    )]
971    WhitespaceInDuration { value: String, byte: u8 },
972    #[error(
973        "duration: value {value:?} contains a non-ASCII Unicode whitespace character \
974         {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :wall-clock` \
975         is `<integer><unit>` (e.g. `\"30s\"`, `\"500ms\"`, `\"2m\"`, `\"1h\"`) with no \
976         whitespace characters anywhere (ASCII or Unicode). A non-ASCII-whitespace-\
977         carrying shape (`\"\\u{{00A0}}30s\"` — paste-from-typography NBSP prefix; \
978         `\"30s\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
979         `\"30\\u{{2003}}s\"` — paste-from-typography EM-SPACE between magnitude and \
980         unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
981         its bytes match the ASCII whitespace set) but `str::trim` (which uses \
982         `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
983         the ASCII byte set) silently strips it at parse entry, and the value round-trips \
984         through `render_duration` to a *different* canonical form (`\"30s\"`) on first \
985         serialize — breaking the THEORY.md Part V render-determinism contract every \
986         typed slot carries. Strip every non-ASCII whitespace character (write `\"30s\"` \
987         verbatim with only ASCII bytes)"
988    )]
989    NonAsciiWhitespaceInDuration {
990        value: String,
991        ch: char,
992        codepoint: u32,
993    },
994    #[error("millicores: bad value {0:?} (expected `<int>m` or `<int>`)")]
995    BadMillicores(String),
996    #[error(
997        "millicores: magnitude {value:?} is not a non-negative integer — the canonical \
998         authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
999         `\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core \
1000         shorthand `<integer>` (e.g. `\"2\"` = `\"2000m\"`), with no decimal point and \
1001         no leading `+` sign. A fractional / decimal-shaped magnitude (`\"1.5\"`, \
1002         `\"500.0m\"`, `\"+500m\"`, `\"-100m\"`) round-trips through `render_millicores` \
1003         to a *different* canonical form (`\"1500m\"`, `\"500m\"`, `\"500m\"`, \
1004         parse-rejection) on first serialize — breaking the THEORY.md Part V \
1005         render-determinism contract every typed slot carries. Pick an integer magnitude \
1006         in millicores (write `\"1500m\"` instead of `\"1.5\"`; `\"500m\"` instead of \
1007         `\"500.0m\"`)"
1008    )]
1009    NonIntegerMillicoreMagnitude { value: String },
1010    #[error(
1011        "millicores: magnitude {value:?} has a non-canonical leading zero — the canonical \
1012         authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
1013         `\"500m\"` for half a core, `\"2000m\"` for two cores) or the bare-core shorthand \
1014         `<integer>` (e.g. `\"2\"` = `\"2000m\"`) with no leading-zero padding on the \
1015         magnitude. A leading-zero magnitude (`\"0500m\"`, `\"00m\"`, `\"02\"`, `\"01500m\"`) \
1016         round-trips through `render_millicores` to a *different* canonical form (`\"500m\"`, \
1017         `\"0m\"`, `\"2000m\"`, `\"1500m\"`) on first serialize — breaking the THEORY.md Part \
1018         V render-determinism contract every typed slot carries. Strip the leading zeros \
1019         (write `\"500m\"` instead of `\"0500m\"`; `\"2\"` instead of `\"02\"`)"
1020    )]
1021    LeadingZeroMillicoreMagnitude { value: String },
1022    #[error(
1023        "millicores: value {value:?} contains whitespace byte 0x{byte:02x} — the canonical \
1024         authoring form for `:limits :cpu` is `<integer>m` (Kubernetes millicores, e.g. \
1025         `\"500m\"`, `\"2000m\"`) or the bare-core shorthand `<integer>` (e.g. `\"2\"`) \
1026         with no whitespace bytes anywhere. A whitespace-carrying shape (`\" 500m\"`, \
1027         `\"500m \"`, `\"500 m\"`, `\"\\t500m\"`, `\"500m\\n\"`) round-trips through \
1028         `render_millicores` to a *different* canonical form (`\"500m\"`) on first \
1029         serialize — breaking the THEORY.md Part V render-determinism contract every \
1030         typed slot carries. Strip every whitespace byte (write `\"500m\"` verbatim)"
1031    )]
1032    WhitespaceInMillicores { value: String, byte: u8 },
1033    #[error(
1034        "millicores: value {value:?} contains a non-ASCII Unicode whitespace character \
1035         {ch:?} (U+{codepoint:04X}) — the canonical authoring form for `:limits :cpu` is \
1036         `<integer>m` (Kubernetes millicores, e.g. `\"500m\"`, `\"2000m\"`) or the \
1037         bare-core shorthand `<integer>` (e.g. `\"2\"`) with no whitespace characters \
1038         anywhere (ASCII or Unicode). A non-ASCII-whitespace-carrying shape \
1039         (`\"\\u{{00A0}}500m\"` — paste-from-typography NBSP prefix; \
1040         `\"500m\\u{{2028}}\"` — paste-from-web-doc line-separator suffix; \
1041         `\"500\\u{{2003}}m\"` — paste-from-typography EM-SPACE between magnitude and \
1042         unit) survives the pre-existing `u8::is_ascii_whitespace` byte-scan (none of \
1043         its bytes match the ASCII whitespace set) but `str::trim` (which uses \
1044         `char::is_whitespace` — the Unicode `White_Space` property, strictly wider than \
1045         the ASCII byte set) silently strips it at parse entry, and the value round-trips \
1046         through `render_millicores` to a *different* canonical form (`\"500m\"`) on \
1047         first serialize — breaking the THEORY.md Part V render-determinism contract \
1048         every typed slot carries. Strip every non-ASCII whitespace character (write \
1049         `\"500m\"` verbatim with only ASCII bytes)"
1050    )]
1051    NonAsciiWhitespaceInMillicores {
1052        value: String,
1053        ch: char,
1054        codepoint: u32,
1055    },
1056    #[error(
1057        ":limits :memory must be > 0 — wasmtime StoreLimits refuses a zero memory cap; omit the field for unbounded"
1058    )]
1059    MemoryZero,
1060    #[error(
1061        ":limits :memory ({bytes} bytes) is below the wasm32-wasip2 linear-memory page size (64 KiB = 65536 bytes) — a sub-page cap cannot hold a single wasm linear memory page, so instantiation of any component declaring `(memory 1)` traps with `memory minimum size of 1 pages exceeds memory limits` and a `(memory 0)` component traps the first `memory.grow(1)`. Pin a value ≥ 64 KiB (e.g. `\"64KiB\"`, `\"1MiB\"`, `\"64MiB\"`) or omit the field for unbounded"
1062    )]
1063    MemoryBelowWasm32Page { bytes: u64 },
1064    #[error(
1065        ":limits :memory ({bytes} bytes) exceeds the wasm32-wasip2 linear-memory ceiling (4 GiB = 4294967296 bytes); pin a value ≤ 4 GiB or omit the field for unbounded"
1066    )]
1067    MemoryExceedsWasm32Cap { bytes: u64 },
1068    #[error(
1069        ":limits :memory ({bytes} bytes) carries a sub-page residue the wasm32-wasip2 \
1070         linear-memory model cannot honor — the wasm spec defines linear memory in \
1071         fixed 64 KiB pages (LIMITS_MEMORY_WASM32_PAGE_BYTES = 65536 bytes) and \
1072         wasmtime's StoreLimits::memory_size is consumed as a page-quantized ceiling: \
1073         the engine can grow at most floor({bytes} / 65536) pages, and the bytes in \
1074         [floor({bytes} / 65536) * 65536, {bytes}] are structural dead space the \
1075         runtime cannot honor. Pin a page-aligned value in 64KiB..=4GiB \
1076         (the canonical authoring magnitudes — `\"64KiB\"`, `\"128KiB\"`, `\"1MiB\"`, \
1077         `\"64MiB\"`, `\"1GiB\"`, `\"4GiB\"` — every power-of-1024 unit the byte-size \
1078         codec emits divides cleanly by the page size) or omit the field for unbounded"
1079    )]
1080    MemoryNotPageMultiple { bytes: u64 },
1081    #[error(
1082        ":limits :fuel must be > 0 — wasmtime traps the first instruction at fuel=0; omit the field for unbounded"
1083    )]
1084    FuelZero,
1085    #[error(
1086        ":limits :fuel ({fuel} instructions) exceeds the per-process ceiling \
1087         (LIMITS_FUEL_MAX = 1_000_000_000_000 = 10^12 wasm instructions) — a value \
1088         above this cap turns the typed per-call fuel counter into a no-op budget: \
1089         the sibling `:wall-clock` cap (LIMITS_WALL_CLOCK_MAX = 1h = 3600s) fires \
1090         before the fuel counter could ever be drained (wasmtime's documented \
1091         fuel-tracked execution rate sits at ~10^8–10^9 fuel-units per second on \
1092         modern x86_64 / aarch64 hosts running wasmtime through Cranelift, so the \
1093         largest realistic per-call fuel budget reachable within 1h sits at ~3.6 × \
1094         10^11–3.6 × 10^12 fuel-units, and a value above 10^12 is structurally \
1095         unreachable as a per-call counter), so the typed `:fuel` slot becomes a \
1096         declared-but-no-op contract far from the source caixa.lisp. Pin a value \
1097         in 1..=1_000_000_000_000 (the canonical caixa Servico runs in the \
1098         10^6..=10^9 fuel band — the in-tree `Caixa::template` documentation and \
1099         `caixa-feira` examples carry `:fuel 1_000_000` = 10^6, peer to \
1100         wasmtime's official `Store::set_fuel(1_000_000)` example in the wasmtime \
1101         book; production-shape per-request fuel budgets sit in the 10^7..=10^9 \
1102         band for compute-bound workloads) or omit :fuel to express `no per-call \
1103         fuel budget on this axis` (the wasm-engine then relies entirely on the \
1104         sibling `:wall-clock` cgroup / Kubernetes activeDeadlineSeconds deadline)"
1105    )]
1106    FuelExceedsCap { fuel: u64 },
1107    #[error(
1108        ":limits :wall-clock must be > 0 — a zero deadline expires before the call starts; omit the field for unbounded"
1109    )]
1110    WallClockZero,
1111    #[error(
1112        ":limits :wall-clock ({wall_clock:?}) carries a sub-millisecond residue the typed `:wall-clock` duration codec cannot round-trip — \
1113         the codec truncates to `as_millis()` before picking the canonical unit, so a value with `subsec_nanos() % 1_000_000 != 0` either \
1114         truncates on first serialize (e.g. `Duration::from_micros(1500)` → \"1ms\" → `Duration::from_millis(1)` ≠ original) or renders \
1115         as \"0s\" the `WallClockZero` arm then rejects on re-validate. Pin an integer-millisecond magnitude in the canonical authoring form \
1116         (`<integer><unit>` for unit ∈ {{ms, s, m, h}}, e.g. `\"500ms\"`, `\"30s\"`, `\"2m\"`, `\"1h\"`) or omit the field for unbounded"
1117    )]
1118    WallClockNotCanonical { wall_clock: Duration },
1119    #[error(
1120        ":limits :wall-clock ({wall_clock:?}) exceeds the per-process ceiling \
1121         (LIMITS_WALL_CLOCK_MAX = 1h = 3600s) — a value above this cap turns the typed \
1122         per-call deadline into a nominal-only contract (the wasm-engine's epoch-deadline \
1123         cancellation reaches for a `Duration` so long no realistic synchronous wasm call \
1124         can hit it), and the MESH-COMPOSITION §V \"no infinite blocking\" CSE invariant \
1125         degenerates to enforcement only at the per-Servico cgroup / Kubernetes \
1126         activeDeadlineSeconds layer — far above the per-call granularity the typed \
1127         `:limits :wall-clock` slot is meant to express. Pin a value in 1ms..=1h \
1128         (Envoy / Istio / Linkerd production per-request playbooks all recommend ≤ 60s; \
1129         AWS App Mesh / ingress-nginx typical ≤ 300s; the longest per-request \
1130         `proxy_read_timeout` ingress-nginx documents maxes out at the same 3600s ceiling) \
1131         or omit :wall-clock to express `no per-process deadline on this axis` (the \
1132         deadline then relies entirely on the cluster-level cgroup / pod \
1133         activeDeadlineSeconds bound)"
1134    )]
1135    WallClockExceedsCap { wall_clock: Duration },
1136    #[error(
1137        ":limits :cpu must be > 0m — a zero cgroup share starves the process; omit the field for unbounded"
1138    )]
1139    CpuZero,
1140    #[error(
1141        ":limits :cpu ({millicores}m) exceeds the per-process ceiling \
1142         (LIMITS_CPU_MILLICORES_MAX = 128_000m = 128 cores) — a value above this cap is \
1143         structurally unschedulable on every commercially-common managed-Kubernetes node \
1144         pool (GKE Standard / EKS managed / AKS default general-purpose SKU ladders top out \
1145         at 128 vCPU per node; AWS m7i.32xlarge / c7i.32xlarge, Azure HBv3-128rs, GCP \
1146         c3-standard-128 all sit at the same 128-vCPU ceiling), so the resulting \
1147         `pleme-computeunit` chart's `resources.requests.cpu` lands as a hint the \
1148         Kubernetes scheduler cannot bind to any node — the pod sits `Pending` indefinitely \
1149         with a `0/N nodes are available: N Insufficient cpu` event, and the typed `:cpu` \
1150         slot becomes an unschedulable contract far from the source caixa.lisp. The \
1151         wasm32-wasip2 single-threaded execution model the canonical caixa Servico targets \
1152         reinforces the structural argument: a single wasm component cannot saturate more \
1153         than one core, so even the Lunatic-style supervised-multi-process host bounds its \
1154         useful CPU request to the host node's vCPU count. Pin a value in 1m..=128000m \
1155         (the canonical caixa Servico runs in the 100m..=2000m band — every in-tree \
1156         example uses 500m; AWS App Mesh / Envoy / Istio per-pod CPU production playbooks \
1157         all sit ≤ 8000m / 8 cores; the longest documented per-Servico CPU request any \
1158         pleme-io substrate playbook recommends maxes at ~16 cores) or omit :cpu to \
1159         express `no per-process CPU hint on this axis` (the cgroup share then defaults to \
1160         the cluster-level `LimitRange` / `ResourceQuota` policy the operator pins on the \
1161         host namespace)"
1162    )]
1163    CpuExceedsCap { millicores: u32 },
1164}
1165
1166// ── byte-size codec ────────────────────────────────────────────────────
1167
1168fn parse_byte_size(s: &str) -> Result<u64, LimitsError> {
1169    // Paired whitespace-rejection arm — the ASCII byte-scan
1170    // (paste-from-aligned-doc leading space, shell-history trailing
1171    // space, typography space between magnitude and unit, block-scalar
1172    // tab, multi-line trailing newline) closes the WhatWG-conformant
1173    // ASCII whitespace bytes (`0x20`, `0x09`, `0x0A`, `0x0C`, `0x0D`);
1174    // the non-ASCII `char::is_whitespace` scan closes the strictly-
1175    // complementary Unicode `White_Space` class (NBSP `\u{00A0}`, LINE
1176    // SEPARATOR `\u{2028}`, EM-SPACE `\u{2003}`, and the peer
1177    // typography codepoints) that `str::trim` at parse entry silently
1178    // strips. Either drift class would round-trip through
1179    // `render_byte_size` to a *different* canonical form on next emit
1180    // — breaking the THEORY.md Part V render-determinism contract every
1181    // typed slot carries. Diagnostics stay typed at
1182    // `WhitespaceInByteSize` / `NonAsciiWhitespaceInByteSize` so the
1183    // failing byte / char + U+XXXX codepoint reaches the author verbatim
1184    // rather than being value-laundered through a downstream
1185    // `BadByteMagnitude` arm.
1186    //
1187    // Routed through the lifted [`crate::render::reject_whitespace`]
1188    // primitive — the substrate-side single-owner gate every typed-
1189    // magnitude codec in caixa-core (`parse_byte_size` /
1190    // `parse_duration` / `parse_millicores` /
1191    // `supervisor::duration_codec` / `rate_limit_codec`) shares. Drift
1192    // between any two codec sites' paired-arm rejection set becomes a
1193    // single-edit fix at the composed predicate rather than five
1194    // independent paired-arm re-inlines diverging over time.
1195    crate::render::reject_whitespace(
1196        s,
1197        |byte| LimitsError::WhitespaceInByteSize {
1198            value: s.into(),
1199            byte,
1200        },
1201        |ch| LimitsError::NonAsciiWhitespaceInByteSize {
1202            value: s.into(),
1203            ch,
1204            codepoint: ch as u32,
1205        },
1206    )?;
1207    let s = s.trim();
1208    if s.is_empty() {
1209        return Err(LimitsError::EmptyByteSize(s.into()));
1210    }
1211    let split_at = s.find(|c: char| c.is_ascii_alphabetic()).unwrap_or(s.len());
1212    let (num_part, unit) = s.split_at(split_at);
1213    let num_trim = num_part.trim();
1214    // The canonical authoring form for `:limits :memory` is
1215    // `<integer><unit>` — every magnitude `render_byte_size` emits is a
1216    // non-negative integer with no decimal point and no leading sign,
1217    // so the parser's accepted set must match for serialize/deserialize
1218    // to round-trip without canonical-form drift. Until this gate
1219    // landed the parser accepted any `f64`-shaped magnitude
1220    // (`"1.5KiB"` → 1536 bytes, `"1.0MiB"` → 1MiB, `"0.5GiB"` → 512MiB,
1221    // `"+1024"` → 1024) and serde silently round-tripped the value to
1222    // a *different* canonical string on the next emit (`"1.5KiB"` →
1223    // 1536 → `"1536"`, `"1.0MiB"` → 1048576 → `"1MiB"`, `"0.5GiB"` →
1224    // 536870912 → `"512MiB"`, `"+1024"` → 1024 → `"1KiB"`) — breaking
1225    // the THEORY.md §V.2.7 render-determinism contract every typed slot
1226    // carries.
1227    //
1228    // Strict canonical form: every byte of the magnitude is an ASCII
1229    // digit (no `.`, no `+`, no `-`). On current Rust `u64::from_str`
1230    // permissively accepts a leading `+` (`"+1024"` → 1024) — that's a
1231    // canonical-drift shape `render_byte_size` never emits, so the
1232    // digit-only check is what closes the leading-sign class; relying
1233    // on `u64::from_str`'s strictness alone would silently admit it.
1234    // On non-digit-only inputs the gate distinguishes "non-canonical-
1235    // but-numeric" (parses as f64 or i64, so it's an authoring-shape
1236    // footgun) from "garbage" (parses as neither, so it's not a
1237    // numeric input at all) — the diagnostic names the offending
1238    // magnitude shape verbatim rather than collapsing both authoring
1239    // footguns into a single opaque `BadByteMagnitude`.
1240    //
1241    // Same canonical-form discipline
1242    // [`crate::AplicacaoSpec::validate_politicas`]'s
1243    // [`is_canonical_rate_limit_window`] gate (808017c) applies to the
1244    // rate-limit `:window` axis — the codec's accepted set matches its
1245    // emitted set, structurally.
1246    //
1247    // (Scientific-notation magnitudes like `"1e3KiB"` are also rejected,
1248    // but on a different arm: the parser splits on the first ASCII-
1249    // alphabetic byte, so the `e` is read as a unit prefix and the
1250    // input falls into the `UnknownByteUnit { unit: "e3KiB" }` branch
1251    // before this gate is consulted — that's the existing diagnostic
1252    // for the scientific-shape footgun, and this gate is additive to
1253    // it.)
1254    //
1255    // Routed through the lifted
1256    // [`crate::render::is_digit_only_magnitude`] predicate — the
1257    // single source of truth every typed-magnitude codec in
1258    // caixa-core (`parse_byte_size` / `parse_duration` /
1259    // `parse_millicores` / `supervisor::duration_codec` /
1260    // `rate_limit_codec`) shares. Drift between any two codec sites'
1261    // digit-only rejection set becomes a single-edit fix at the
1262    // shared predicate rather than five independent
1263    // `!<var>.is_empty() && <var>.bytes().all(|b| b.is_ascii_digit())`
1264    // scans diverging over time — same "single lifted source of truth"
1265    // discipline the peer canonical-form predicates
1266    // ([`crate::render::find_ascii_whitespace_byte`] /
1267    // [`crate::render::find_non_ascii_whitespace_char`] /
1268    // [`crate::render::is_leading_zero_padded_magnitude`]) carry on
1269    // the whitespace and leading-zero-padding drift-class axes.
1270    let digit_only = crate::render::is_digit_only_magnitude(num_trim);
1271    if !digit_only {
1272        // Distinguish "non-canonical-but-numeric" (`"1.5"`, `"1.0"`,
1273        // `"+1024"`, `"-1"`) from "garbage" (`"abc"`, `"--1"`) so the
1274        // diagnostic names the offending magnitude shape verbatim.
1275        // Use f64 + i64 fallbacks for the "numeric" detection so every
1276        // non-digit-only-but-parseable input lands on
1277        // `NonIntegerByteMagnitude` regardless of sign or fractionality.
1278        let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
1279        if numeric {
1280            return Err(LimitsError::NonIntegerByteMagnitude {
1281                value: num_trim.into(),
1282            });
1283        }
1284        return Err(LimitsError::BadByteMagnitude(num_part.into()));
1285    }
1286    // Leading-zero arm — peer with the `parse_duration` leading-zero
1287    // arm (39762d7), the `supervisor::duration_codec` leading-zero arm
1288    // (9178904) and the `rate_limit_codec` leading-zero arm (4f46830)
1289    // on the same canonical-form render-determinism axis. The
1290    // digit-only gate accepts `"0064MiB"`, `"01024"`, `"00KiB"`,
1291    // `"0500MB"` as `u64::from_str` parses them losslessly (= 64, 1024,
1292    // 0, 500), but `render_byte_size` emits the leading-zero-stripped
1293    // form (`"64MiB"`, `"1KiB"`, `"0"`, `"500MB"`) — a *different*
1294    // canonical string on the next emit, breaking the THEORY.md Part V
1295    // render-determinism contract the same way `"+1024"` did before the
1296    // leading-`+` arm landed. The single-byte magnitude `"0"` (or
1297    // `"0B"` / `"0KiB"`) round-trips losslessly through
1298    // `render_byte_size` (`render_byte_size(0)` emits `"0"`) — the
1299    // downstream semantic-zero gate [`LimitsError::MemoryZero`] refuses
1300    // zero-magnitude authoring at the typed-validate layer above, so
1301    // the single-byte `"0"` stays in the accepted set at this codec
1302    // layer and the diagnostic partitioning between canonical-form
1303    // drift (this arm) and semantic-zero (the downstream gate) remains
1304    // stable. Same codec-layer / typed-validate-layer partition the
1305    // peer codecs preserve.
1306    //
1307    // Routed through the lifted
1308    // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1309    // the single source of truth every typed-magnitude codec in
1310    // caixa-core (`parse_byte_size` / `parse_duration` /
1311    // `parse_millicores` / `supervisor::duration_codec` /
1312    // `rate_limit_codec`) shares. Drift between any two codec sites'
1313    // leading-zero rejection set becomes a single-edit fix at the
1314    // shared predicate rather than five independent
1315    // `s.len() > 1 && s.as_bytes()[0] == b'0'` scans diverging over
1316    // time — same "single lifted source of truth" discipline the
1317    // peer whitespace predicates
1318    // ([`crate::render::find_ascii_whitespace_byte`] /
1319    // [`crate::render::find_non_ascii_whitespace_char`]) carry on
1320    // their strictly-complementary axes.
1321    if crate::render::is_leading_zero_padded_magnitude(num_trim) {
1322        return Err(LimitsError::LeadingZeroByteMagnitude {
1323            value: num_trim.into(),
1324        });
1325    }
1326    // `digit_only` guarantees every byte is `[0-9]`, so the only way
1327    // u64::from_str can fail here is overflow (the magnitude exceeds
1328    // u64::MAX). Surface that as `BadByteMagnitude` with an overflow-
1329    // shaped wording so the diagnostic names the offending magnitude
1330    // verbatim rather than collapsing onto the non-canonical arm.
1331    let num: u64 = num_trim.parse::<u64>().map_err(|_| {
1332        LimitsError::BadByteMagnitude(format!("{num_trim} (digit-only magnitude overflows u64)"))
1333    })?;
1334    let multiplier: u64 = match unit.trim() {
1335        "" | "B" => 1,
1336        "KB" => 1_000,
1337        "MB" => 1_000_000,
1338        "GB" => 1_000_000_000,
1339        "KiB" => 1024,
1340        "MiB" => 1024 * 1024,
1341        "GiB" => 1024 * 1024 * 1024,
1342        other => {
1343            return Err(LimitsError::UnknownByteUnit { unit: other.into() });
1344        }
1345    };
1346    // Overflow surfaces as `BadByteMagnitude` (a u64-saturating
1347    // multiply would silently truncate to `u64::MAX` and then the
1348    // wasm32-cap gate at validate time would catch it — but a u64
1349    // overflow is a parse-shaped failure on the author's input, not a
1350    // domain-cap rejection on a well-formed value, so it surfaces here
1351    // as a parser diagnostic naming the offending magnitude × unit
1352    // pair rather than as `MemoryExceedsWasm32Cap { bytes: u64::MAX }`
1353    // far from the author's intent).
1354    num.checked_mul(multiplier).ok_or_else(|| {
1355        LimitsError::BadByteMagnitude(format!(
1356            "{num_trim}{unit_trim} overflows u64 (magnitude × unit > 2^64-1)",
1357            unit_trim = unit.trim()
1358        ))
1359    })
1360}
1361
1362fn render_byte_size(n: u64) -> String {
1363    // Prefer the largest power-of-1024 unit that divides cleanly; fall
1364    // back to bytes if nothing matches.
1365    const UNITS: &[(u64, &str)] = &[
1366        (1024 * 1024 * 1024, "GiB"),
1367        (1024 * 1024, "MiB"),
1368        (1024, "KiB"),
1369    ];
1370    for (mult, label) in UNITS {
1371        if n >= *mult && n.is_multiple_of(*mult) {
1372            return format!("{}{label}", n / mult);
1373        }
1374    }
1375    format!("{n}")
1376}
1377
1378fn ser_byte_size<S: Serializer>(v: &Option<u64>, s: S) -> Result<S::Ok, S::Error> {
1379    // Route through the canonical [`crate::render::serialize_option_via_str`]
1380    // — the substrate-side single-owner primitive for the forward arm
1381    // of the typed-magnitude codec family. See its docstring for the
1382    // full sibling roster and the compounding rationale that pins this
1383    // lift; load-bearing pinned by
1384    // `tests::ser_byte_size_routes_through_render_serialize_option_via_str_canonical`.
1385    crate::render::serialize_option_via_str(v, s, render_byte_size)
1386}
1387
1388fn de_byte_size<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u64>, D::Error> {
1389    // Route through the canonical [`crate::render::deserialize_option_via_str`]
1390    // — the substrate-side single-owner primitive for the reverse arm
1391    // of the typed-magnitude codec family. See its docstring for the
1392    // full sibling roster and the compounding rationale that pins this
1393    // lift; load-bearing pinned by
1394    // `tests::de_byte_size_routes_through_render_deserialize_option_via_str_canonical`.
1395    crate::render::deserialize_option_via_str(d, parse_byte_size)
1396}
1397
1398// ── duration codec ─────────────────────────────────────────────────────
1399
1400fn parse_duration(s: &str) -> Result<Duration, LimitsError> {
1401    // Paired whitespace-rejection arm — same canonical-form
1402    // render-determinism discipline as the peer `parse_byte_size` /
1403    // `parse_millicores` / `supervisor::duration_codec::parse` /
1404    // `rate_limit_codec::parse` sites: the ASCII byte-scan closes the
1405    // WhatWG-conformant whitespace bytes every downstream YAML / JSON /
1406    // TOML parser can feed through a quoted-scalar value verbatim
1407    // (`0x20`, `0x09`, `0x0A`, `0x0C`, `0x0D`), the non-ASCII
1408    // `char::is_whitespace` scan closes the strictly-complementary
1409    // Unicode `White_Space` class (NBSP `\u{00A0}`, LINE SEPARATOR
1410    // `\u{2028}`, EM-SPACE `\u{2003}`, and the peer typography
1411    // codepoints) that `str::trim` at parse entry silently strips.
1412    // Either drift class would round-trip through `render_duration` to
1413    // a *different* canonical form on next emit — breaking the
1414    // THEORY.md Part V render-determinism contract. Diagnostics stay
1415    // typed at `WhitespaceInDuration` / `NonAsciiWhitespaceInDuration`.
1416    //
1417    // Routed through the lifted [`crate::render::reject_whitespace`]
1418    // primitive — the substrate-side single-owner paired-arm gate every
1419    // typed-magnitude codec in caixa-core shares.
1420    crate::render::reject_whitespace(
1421        s,
1422        |byte| LimitsError::WhitespaceInDuration {
1423            value: s.into(),
1424            byte,
1425        },
1426        |ch| LimitsError::NonAsciiWhitespaceInDuration {
1427            value: s.into(),
1428            ch,
1429            codepoint: ch as u32,
1430        },
1431    )?;
1432    let s = s.trim();
1433    if s.is_empty() {
1434        return Err(LimitsError::EmptyDuration(s.into()));
1435    }
1436    let split_at = s.find(|c: char| c.is_ascii_alphabetic()).unwrap_or(s.len());
1437    let (num_part, unit) = s.split_at(split_at);
1438    let num_trim = num_part.trim();
1439    // The canonical authoring form for `:limits :wall-clock` is
1440    // `<integer><unit>` — every magnitude `render_duration` emits is a
1441    // non-negative integer with no decimal point and no leading sign,
1442    // so the parser's accepted set must match for serialize/deserialize
1443    // to round-trip without canonical-form drift. Until this gate
1444    // landed the parser accepted any `f64`-shaped magnitude
1445    // (`"1.5s"` → 1500ms, `"1.0s"` → 1s, `"0.5m"` → 30s, `"+30s"` →
1446    // 30s) and serde silently round-tripped the value to a *different*
1447    // canonical string on the next emit (`"1.5s"` → 1500ms →
1448    // `"1500ms"`, `"1.0s"` → 1s → `"1s"`, `"0.5m"` → 30s → `"30s"`,
1449    // `"+30s"` → 30s → `"30s"`) — breaking the THEORY.md Part V
1450    // render-determinism contract every typed slot carries. The same
1451    // canonical-form discipline `parse_byte_size`'s integer-magnitude
1452    // gate (the immediate predecessor on the peer `:limits :memory`
1453    // codec) applies; this gate is the direct successor on the
1454    // `:limits :wall-clock` codec.
1455    //
1456    // Strict canonical form: every byte of the magnitude is an ASCII
1457    // digit (no `.`, no `+`, no `-`). On current Rust `u64::from_str`
1458    // permissively accepts a leading `+` (`"+30"` → 30) — that's a
1459    // canonical-drift shape `render_duration` never emits, so the
1460    // digit-only check is what closes the leading-sign class; relying
1461    // on `u64::from_str`'s strictness alone would silently admit it.
1462    // On non-digit-only inputs the gate distinguishes "non-canonical-
1463    // but-numeric" (parses as f64 or i64 — surfaced as the new
1464    // `NonIntegerDurationMagnitude` variant with a self-locating
1465    // diagnostic) from "garbage" (parses as neither — surfaced as the
1466    // existing `BadDurationMagnitude` so its narrower diagnostic
1467    // remains load-bearing).
1468    //
1469    // Routed through the lifted
1470    // [`crate::render::is_digit_only_magnitude`] predicate — the same
1471    // source of truth the four peer typed-magnitude codec sites share.
1472    let digit_only = crate::render::is_digit_only_magnitude(num_trim);
1473    if !digit_only {
1474        let numeric = num_trim.parse::<f64>().is_ok() || num_trim.parse::<i64>().is_ok();
1475        if numeric {
1476            return Err(LimitsError::NonIntegerDurationMagnitude {
1477                value: num_trim.into(),
1478            });
1479        }
1480        return Err(LimitsError::BadDurationMagnitude(num_part.into()));
1481    }
1482    // Leading-zero arm — peer with the `supervisor::duration_codec`
1483    // leading-zero arm (9178904) and the `rate_limit_codec`
1484    // leading-zero arm (4f46830) on the same canonical-form
1485    // render-determinism axis. The digit-only gate accepts `"030s"`,
1486    // `"00s"`, `"01h"`, `"0500ms"` as `u64::from_str` parses them
1487    // losslessly (= 30, 0, 1, 500), but `render_duration` emits the
1488    // leading-zero-stripped form (`"30s"`, `"0s"`, `"1h"`, `"500ms"`)
1489    // — a *different* canonical string on the next emit, breaking the
1490    // THEORY.md Part V render-determinism contract the same way
1491    // `"+30s"` did before the leading-`+` arm landed. The single-byte
1492    // magnitude `"0"` (or `"0s"` / `"0ms"`) round-trips losslessly
1493    // through `render_duration` (`render_duration(Duration::ZERO)`
1494    // emits `"0s"`) — the downstream semantic-zero gate
1495    // [`LimitsError::WallClockZero`] refuses zero-magnitude authoring
1496    // at the typed-validate layer above, so the single-byte `"0"`
1497    // stays in the accepted set at this codec layer and the
1498    // diagnostic partitioning between canonical-form drift (this arm)
1499    // and semantic-zero (the downstream gate) remains stable. Same
1500    // codec-layer / typed-validate-layer partition the peer codecs
1501    // preserve.
1502    //
1503    // Routed through the lifted
1504    // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1505    // the same source of truth the four peer typed-magnitude codec
1506    // sites share.
1507    if crate::render::is_leading_zero_padded_magnitude(num_trim) {
1508        return Err(LimitsError::LeadingZeroDurationMagnitude {
1509            value: num_trim.into(),
1510        });
1511    }
1512    // The digit-only gate guarantees every byte is `[0-9]`, and the
1513    // leading-zero arm above guarantees the magnitude is either the
1514    // single byte `"0"` or starts with `[1-9]`, so the only way
1515    // `u64::from_str` can fail here is overflow.
1516    let num: u64 = num_trim.parse::<u64>().map_err(|_| {
1517        LimitsError::BadDurationMagnitude(format!(
1518            "{num_trim} (digit-only magnitude overflows u64)"
1519        ))
1520    })?;
1521    // Multiply on u64 with overflow detection — every unit conversion
1522    // is integer-exact for an integer magnitude, so the codec drops
1523    // `Duration::from_secs_f64` entirely. Overflow surfaces at parse
1524    // time with a parser-shaped diagnostic naming the offending
1525    // magnitude × unit pair (matches `parse_byte_size`'s overflow arm).
1526    let unit_trim = unit.trim();
1527    let dur = match unit_trim {
1528        "ms" => Duration::from_millis(num),
1529        "s" | "" => Duration::from_secs(num),
1530        "m" => Duration::from_secs(num.checked_mul(60).ok_or_else(|| {
1531            LimitsError::BadDurationMagnitude(format!(
1532                "{num_trim}{unit_trim} overflows u64 (magnitude × 60 > 2^64-1)"
1533            ))
1534        })?),
1535        "h" => Duration::from_secs(num.checked_mul(3600).ok_or_else(|| {
1536            LimitsError::BadDurationMagnitude(format!(
1537                "{num_trim}{unit_trim} overflows u64 (magnitude × 3600 > 2^64-1)"
1538            ))
1539        })?),
1540        other => {
1541            return Err(LimitsError::UnknownDurationUnit { unit: other.into() });
1542        }
1543    };
1544    Ok(dur)
1545}
1546
1547fn ser_duration<S: Serializer>(v: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
1548    // Route through the canonical [`crate::render::serialize_option_via_str`]
1549    // — the substrate-side single-owner primitive for the forward arm
1550    // of the typed-magnitude codec family — around the canonical
1551    // [`crate::supervisor::duration_codec::render`] duration-byte
1552    // dispatch. The `render` dispatch is itself the load-bearing
1553    // single-owner primitive for duration bytes across every caixa
1554    // typed-duration surface (`:limits :wall-clock`,
1555    // `:politicas :timeout`, `:circuit-breaker :window`, future OTP
1556    // `gen_server` per-call timeouts); the outer
1557    // `serialize_option_via_str` closes the `Some(_) => serialize_str`
1558    // / `None => serialize_none` `Option`-arm dispatch every peer
1559    // typed-magnitude serializer shares. Load-bearing pinned by
1560    // `tests::ser_duration_routes_through_supervisor_duration_codec_render_canonical`.
1561    crate::render::serialize_option_via_str(v, s, crate::supervisor::duration_codec::render)
1562}
1563
1564fn de_duration<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
1565    // Route through the canonical [`crate::render::deserialize_option_via_str`]
1566    // — the substrate-side single-owner primitive for the reverse arm
1567    // of the typed-magnitude codec family. See its docstring for the
1568    // full sibling roster and the compounding rationale that pins this
1569    // lift.
1570    crate::render::deserialize_option_via_str(d, parse_duration)
1571}
1572
1573// ── millicores codec ───────────────────────────────────────────────────
1574
1575fn parse_millicores(s: &str) -> Result<u32, LimitsError> {
1576    // Paired whitespace-rejection arm — same canonical-form
1577    // render-determinism discipline as the peer `parse_byte_size` /
1578    // `parse_duration` / `supervisor::duration_codec::parse` /
1579    // `rate_limit_codec::parse` sites: the ASCII byte-scan closes the
1580    // WhatWG-conformant whitespace bytes (`0x20`, `0x09`, `0x0A`,
1581    // `0x0C`, `0x0D`), the non-ASCII `char::is_whitespace` scan closes
1582    // the strictly-complementary Unicode `White_Space` class (NBSP
1583    // `\u{00A0}`, LINE SEPARATOR `\u{2028}`, EM-SPACE `\u{2003}`, and
1584    // the peer typography codepoints) that `str::trim` at parse entry
1585    // silently strips. Either drift class would round-trip through
1586    // `render_millicores` to a *different* canonical form on next emit
1587    // — breaking the THEORY.md Part V render-determinism contract.
1588    // Diagnostics stay typed at `WhitespaceInMillicores` /
1589    // `NonAsciiWhitespaceInMillicores` — peer with every prior
1590    // canonical-form-drift arm on this codec
1591    // (`NonIntegerMillicoreMagnitude`, `LeadingZeroMillicoreMagnitude`).
1592    //
1593    // Routed through the lifted [`crate::render::reject_whitespace`]
1594    // primitive — the substrate-side single-owner paired-arm gate every
1595    // typed-magnitude codec in caixa-core shares.
1596    crate::render::reject_whitespace(
1597        s,
1598        |byte| LimitsError::WhitespaceInMillicores {
1599            value: s.into(),
1600            byte,
1601        },
1602        |ch| LimitsError::NonAsciiWhitespaceInMillicores {
1603            value: s.into(),
1604            ch,
1605            codepoint: ch as u32,
1606        },
1607    )?;
1608    let s_trim = s.trim();
1609    if s_trim.is_empty() {
1610        return Err(LimitsError::BadMillicores(s.into()));
1611    }
1612    let (magnitude, has_m_suffix) = match s_trim.strip_suffix('m') {
1613        Some(stripped) => (stripped.trim(), true),
1614        None => (s_trim, false),
1615    };
1616    if magnitude.is_empty() {
1617        // Bare `"m"` (or `" m "`) — no magnitude was authored. The
1618        // canonical millicores authoring form requires a magnitude in
1619        // front of the unit (`"500m"`, not `"m"`). Surface as
1620        // `BadMillicores` so the existing narrower-arm wording stays
1621        // load-bearing for "no recognizable magnitude" inputs.
1622        return Err(LimitsError::BadMillicores(s.into()));
1623    }
1624    // The canonical authoring form for `:limits :cpu` is `<integer>m`
1625    // (Kubernetes millicores) or the bare-core shorthand `<integer>`
1626    // (`"2"` = 2000 millicores). Every magnitude `render_millicores`
1627    // emits is a non-negative integer (`format!("{m}m")`) — no decimal
1628    // point, no leading sign — so the parser's accepted set must match
1629    // for serialize/deserialize to round-trip without canonical-form
1630    // drift. Until this gate landed the parser accepted any
1631    // `u32::from_str`-shaped magnitude (`"+500m"` → 500, `"+2"` →
1632    // 2000) and serde silently round-tripped the value to a *different*
1633    // canonical string on the next emit (`"+500m"` → `"500m"`, `"+2"`
1634    // → `"2000m"`) — breaking the THEORY.md Part V render-determinism
1635    // contract every typed slot carries. Closes the sixth (and last)
1636    // typed-codec surface in caixa-core on the integer-magnitude
1637    // canonical-form axis, peer with the five duration / byte-size /
1638    // rate-limit codecs the prior trajectory (1c55a2a / 818dd38 /
1639    // d1fd67b / f479c41 / d53c922) covered.
1640    //
1641    // Strict canonical form: every byte of the magnitude is an ASCII
1642    // digit (no `.`, no `+`, no `-`). On current Rust `u32::from_str`
1643    // permissively accepts a leading `+` (`"+500"` → 500) — that's a
1644    // canonical-drift shape `render_millicores` never emits, so the
1645    // digit-only check is what closes the leading-sign class; relying
1646    // on `u32::from_str`'s strictness alone would silently admit it.
1647    // On non-digit-only inputs the gate distinguishes "non-canonical-
1648    // but-numeric" (parses as f64 or i64 — surfaced as the new
1649    // `NonIntegerMillicoreMagnitude` variant naming the offending
1650    // magnitude verbatim with the canonical-form remediation) from
1651    // "garbage" (parses as neither — surfaced as the existing
1652    // `BadMillicores` so its narrower diagnostic shape remains
1653    // load-bearing for the not-a-numeric-input class).
1654    //
1655    // Routed through the lifted
1656    // [`crate::render::is_digit_only_magnitude`] predicate — the same
1657    // source of truth the four peer typed-magnitude codec sites share.
1658    // The predicate carries a `!<var>.is_empty()` gate that is
1659    // strictly no-op here (the `magnitude.is_empty()` arm above
1660    // already surfaces an empty magnitude as
1661    // [`LimitsError::BadMillicores`] before this line is reached), so
1662    // the semantics are preserved verbatim: on every reachable input
1663    // the predicate returns `magnitude.bytes().all(|b|
1664    // b.is_ascii_digit())`, byte-for-byte what the removed inline
1665    // expression computed.
1666    let digit_only = crate::render::is_digit_only_magnitude(magnitude);
1667    if !digit_only {
1668        let numeric = magnitude.parse::<f64>().is_ok() || magnitude.parse::<i64>().is_ok();
1669        if numeric {
1670            return Err(LimitsError::NonIntegerMillicoreMagnitude {
1671                value: magnitude.into(),
1672            });
1673        }
1674        return Err(LimitsError::BadMillicores(s.into()));
1675    }
1676    // Leading-zero arm — peer with the `parse_byte_size` leading-zero
1677    // arm (cea9a78), the `parse_duration` leading-zero arm (39762d7),
1678    // the `supervisor::duration_codec` leading-zero arm (9178904) and
1679    // the `rate_limit_codec` leading-zero arm (4f46830) on the same
1680    // canonical-form render-determinism axis. The digit-only gate
1681    // accepts `"0500m"`, `"00m"`, `"02"`, `"01500m"` as `u32::from_str`
1682    // parses them losslessly (= 500, 0, 2, 1500), but `render_millicores`
1683    // emits the leading-zero-stripped form (`"500m"`, `"0m"`, `"2000m"`,
1684    // `"1500m"`) — a *different* canonical string on the next emit,
1685    // breaking the THEORY.md Part V render-determinism contract the
1686    // same way `"+500m"` did before the leading-`+` arm landed. The
1687    // single-byte magnitude `"0"` (or `"0m"`) round-trips losslessly
1688    // through `render_millicores` (`render_millicores(0)` emits `"0m"`)
1689    // — the downstream semantic-zero gate [`LimitsError::CpuZero`]
1690    // refuses zero-magnitude authoring at the typed-validate layer
1691    // above, so the single-byte `"0"` stays in the accepted set at this
1692    // codec layer and the diagnostic partitioning between canonical-
1693    // form drift (this arm) and semantic-zero (the downstream gate)
1694    // remains stable. Same codec-layer / typed-validate-layer partition
1695    // the peer codecs preserve. Closes the sixth (and last) typed
1696    // numeric-codec surface in caixa-core on the integer-magnitude
1697    // leading-zero axis — the trajectory the prior `parse_byte_size`
1698    // arm (cea9a78) explicitly named.
1699    //
1700    // Routed through the lifted
1701    // [`crate::render::is_leading_zero_padded_magnitude`] predicate —
1702    // the same source of truth the four peer typed-magnitude codec
1703    // sites share.
1704    if crate::render::is_leading_zero_padded_magnitude(magnitude) {
1705        return Err(LimitsError::LeadingZeroMillicoreMagnitude {
1706            value: magnitude.into(),
1707        });
1708    }
1709    // The digit-only gate guarantees every byte is `[0-9]`, and the
1710    // leading-zero arm above guarantees the magnitude is either the
1711    // single byte `"0"` or starts with `[1-9]`, so the only way
1712    // `u32::from_str` can fail here is overflow (the magnitude exceeds
1713    // `u32::MAX`). Surface that as `BadMillicores` with an overflow-
1714    // shaped wording so the diagnostic names the offending magnitude
1715    // verbatim rather than collapsing onto the non-canonical arm —
1716    // matches `parse_byte_size` / `parse_duration` / `rate_limit_codec`
1717    // overflow-arm shape on the peer typed codecs.
1718    let num: u32 = magnitude.parse::<u32>().map_err(|_| {
1719        LimitsError::BadMillicores(format!("{magnitude} (digit-only magnitude overflows u32)"))
1720    })?;
1721    if has_m_suffix {
1722        Ok(num)
1723    } else {
1724        // Bare-core shorthand: `"2"` = 2000 millicores. Use
1725        // `checked_mul` (not the prior `saturating_mul`) so a
1726        // magnitude that overflows u32 on the × 1000 conversion
1727        // surfaces a parser-shaped diagnostic at parse time rather
1728        // than silently saturating to `u32::MAX` (which would land
1729        // as the cap value far from the author's intent and bypass
1730        // any future validate-time upper-bound gate the `:cpu` axis
1731        // grows). Matches `parse_byte_size`'s overflow-arm shape on
1732        // the magnitude × unit multiply.
1733        num.checked_mul(1000).ok_or_else(|| {
1734            LimitsError::BadMillicores(format!(
1735                "{magnitude} cores × 1000 overflows u32 (write the value in millicores: max \"{}m\")",
1736                u32::MAX
1737            ))
1738        })
1739    }
1740}
1741
1742fn render_millicores(m: u32) -> String {
1743    format!("{m}m")
1744}
1745
1746fn ser_millicores<S: Serializer>(v: &Option<u32>, s: S) -> Result<S::Ok, S::Error> {
1747    // Route through the canonical [`crate::render::serialize_option_via_str`]
1748    // — see peer `ser_byte_size` / `ser_duration` routing notes above.
1749    crate::render::serialize_option_via_str(v, s, render_millicores)
1750}
1751
1752fn de_millicores<'de, D: Deserializer<'de>>(d: D) -> Result<Option<u32>, D::Error> {
1753    // Route through the canonical [`crate::render::deserialize_option_via_str`]
1754    // — see peer `de_byte_size` / `de_duration` routing notes above.
1755    crate::render::deserialize_option_via_str(d, parse_millicores)
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760    use super::*;
1761
1762    #[test]
1763    fn parse_byte_size_known_units() {
1764        assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
1765        assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
1766        assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
1767        assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
1768        assert_eq!(parse_byte_size("1024").unwrap(), 1024);
1769    }
1770
1771    #[test]
1772    fn parse_byte_size_rejects_unknown() {
1773        assert!(matches!(
1774            parse_byte_size("1YiB"),
1775            Err(LimitsError::UnknownByteUnit { .. })
1776        ));
1777        assert!(matches!(
1778            parse_byte_size("not-a-number"),
1779            Err(LimitsError::BadByteMagnitude(_))
1780        ));
1781    }
1782
1783    #[test]
1784    fn parse_duration_known_units() {
1785        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
1786        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
1787        assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
1788        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
1789    }
1790
1791    #[test]
1792    fn parse_millicores_both_forms() {
1793        assert_eq!(parse_millicores("500m").unwrap(), 500);
1794        assert_eq!(parse_millicores("2").unwrap(), 2000);
1795    }
1796
1797    #[test]
1798    fn render_byte_size_canonical() {
1799        assert_eq!(render_byte_size(64 * 1024 * 1024), "64MiB");
1800        assert_eq!(render_byte_size(1024 * 1024 * 1024), "1GiB");
1801        assert_eq!(render_byte_size(1024), "1KiB");
1802        assert_eq!(render_byte_size(123), "123");
1803    }
1804
1805    #[test]
1806    fn ser_byte_size_routes_through_render_serialize_option_via_str_canonical() {
1807        // Routing pin: `ser_byte_size` (the `#[serde(serialize_with = …)]`
1808        // hook on `LimitsSpec::memory`) MUST emit exactly the bytes the
1809        // canonical `crate::render::serialize_option_via_str` primitive
1810        // produces when threaded through the peer `render_byte_size`
1811        // dispatch. Any future accidental re-inline of a bespoke
1812        // `match v { Some(_) => s.serialize_str(_), None =>
1813        // s.serialize_none() }` block inside this module — the shape
1814        // this lift removed — surfaces here as a byte-value drift on
1815        // the very first canonical form the two implementations
1816        // disagree on. Peer of
1817        // `ser_duration_routes_through_supervisor_duration_codec_render_canonical`
1818        // on the sibling `LimitsSpec::wall_clock` axis; same "one
1819        // canonical dispatch per axis, thin projections at each
1820        // consumer" discipline the sibling caixa-core substrate
1821        // primitives already carry.
1822        for n in [
1823            0u64,
1824            1,
1825            1023,
1826            1024,
1827            64 * 1024 * 1024,
1828            4 * 1024 * 1024 * 1024,
1829        ] {
1830            let limits = LimitsSpec {
1831                memory: Some(n),
1832                fuel: None,
1833                wall_clock: None,
1834                cpu: None,
1835            };
1836            let json: serde_json::Value =
1837                serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
1838            let emitted = json[crate::render::M2_LIMITS_KEY_MEMORY]
1839                .as_str()
1840                .expect("memory must serialize to a string");
1841            let canonical = render_byte_size(n);
1842            assert_eq!(
1843                emitted, canonical,
1844                "ser_byte_size drifted from render_byte_size via \
1845                 serialize_option_via_str on {n} bytes",
1846            );
1847        }
1848    }
1849
1850    #[test]
1851    fn de_byte_size_routes_through_render_deserialize_option_via_str_canonical() {
1852        // Routing pin: `de_byte_size` (the
1853        // `#[serde(deserialize_with = …)]` hook on
1854        // `LimitsSpec::memory`) MUST accept exactly the canonical
1855        // string set the peer `parse_byte_size` function accepts, and
1856        // reject everything else with the parser's typed `LimitsError`
1857        // surfaced through `serde::de::Error::custom` — the shape the
1858        // lifted `crate::render::deserialize_option_via_str` primitive
1859        // enforces. A future accidental re-inline of a bespoke `let
1860        // opt: Option<String> = Option::deserialize(d)?; match opt {
1861        // … }` block inside this module — the shape this lift removed
1862        // — that drifted on either arm (silently accepting a value the
1863        // parser rejects, or swallowing a parser error as `Ok(None)`)
1864        // surfaces here.
1865        for raw in ["64MiB", "1024", "0", "4GiB"] {
1866            let field = crate::render::M2_LIMITS_KEY_MEMORY;
1867            let payload = format!("{{\"{field}\":\"{raw}\"}}");
1868            let limits: LimitsSpec =
1869                serde_json::from_str(&payload).expect("canonical memory string must round-trip");
1870            let canonical = parse_byte_size(raw).expect("parse_byte_size accepts canonical form");
1871            assert_eq!(
1872                limits.memory,
1873                Some(canonical),
1874                "de_byte_size drifted from parse_byte_size via \
1875                 deserialize_option_via_str on {raw:?}",
1876            );
1877        }
1878        // Null-arm pin: `null` folds to `None` without invoking the
1879        // parser — the exact contract the lifted primitive's null-arm
1880        // test pins.
1881        let field = crate::render::M2_LIMITS_KEY_MEMORY;
1882        let null_payload = format!("{{\"{field}\":null}}");
1883        let empty: LimitsSpec = serde_json::from_str(&null_payload)
1884            .expect("null memory field must fold to LimitsSpec::memory = None");
1885        assert_eq!(
1886            empty.memory, None,
1887            "de_byte_size must fold null → None via \
1888             deserialize_option_via_str's null-arm",
1889        );
1890        // Reject-arm pin: a bogus string surfaces the parser's error
1891        // through `serde::de::Error::custom` — not `Ok(None)`.
1892        let bad_payload = format!("{{\"{field}\":\"64XiB\"}}");
1893        let err = serde_json::from_str::<LimitsSpec>(&bad_payload)
1894            .expect_err("bogus memory string must surface the parser's error");
1895        let err_text = err.to_string();
1896        assert!(
1897            err_text.contains("64XiB") || err_text.contains("XiB"),
1898            "de_byte_size must surface parse_byte_size's typed \
1899             LimitsError through serde::de::Error::custom — got \
1900             {err_text:?}",
1901        );
1902    }
1903
1904    #[test]
1905    fn ser_duration_routes_through_supervisor_duration_codec_render_canonical() {
1906        // Routing pin: `ser_duration` (the `#[serde(serialize_with = …)]`
1907        // hook on `LimitsSpec::wall_clock`) MUST emit exactly the bytes
1908        // the canonical `crate::supervisor::duration_codec::render`
1909        // primitive produces. Any future accidental re-introduction of a
1910        // sibling free-function `render_duration` shadow inside this
1911        // module — or a per-slot `serialize_with` closure that inlines
1912        // its own magnitude/unit decision tree — surfaces here as a
1913        // byte-value drift on the very first canonical form the two
1914        // implementations disagree on, well before the drift reaches any
1915        // downstream renderer's `wall_clock:` overlay. Same "one
1916        // canonical dispatch per axis, thin projections at each consumer"
1917        // discipline the sibling caixa-core substrate primitives already
1918        // carry on the peer WIT-shape / M2 supervisor-strategy / M3
1919        // mesh-slot free-function classifier families.
1920        for d in [
1921            Duration::from_secs(30),
1922            Duration::from_millis(500),
1923            Duration::from_secs(120),
1924            Duration::from_secs(3600),
1925            Duration::from_millis(0),
1926            Duration::from_millis(1500),
1927        ] {
1928            let limits = LimitsSpec {
1929                memory: None,
1930                fuel: None,
1931                wall_clock: Some(d),
1932                cpu: None,
1933            };
1934            let json: serde_json::Value =
1935                serde_json::from_str(&serde_json::to_string(&limits).unwrap()).unwrap();
1936            let emitted = json[crate::render::M2_LIMITS_KEY_WALL_CLOCK]
1937                .as_str()
1938                .expect("wall_clock must serialize to a string");
1939            let canonical = crate::supervisor::duration_codec::render(d);
1940            assert_eq!(
1941                emitted, canonical,
1942                "ser_duration drifted from supervisor::duration_codec::render on {d:?}",
1943            );
1944        }
1945    }
1946
1947    #[test]
1948    fn parse_byte_size_routes_whitespace_through_render_reject_whitespace_canonical() {
1949        // Routing pin: the paired whitespace-rejection block at the
1950        // top of `parse_byte_size` MUST route through the substrate-
1951        // side [`crate::render::reject_whitespace`] primitive — the
1952        // single-owner paired-arm gate every typed-magnitude codec
1953        // in caixa-core shares. Any future accidental re-inline of a
1954        // bespoke
1955        //
1956        // ```ignore
1957        // if let Some(byte) = find_ascii_whitespace_byte(s) { … }
1958        // if let Some(ch)   = find_non_ascii_whitespace_char(s)  { … }
1959        // ```
1960        //
1961        // block inside this module — the shape this lift removed —
1962        // that drifted on either arm surfaces here as a variant-shape
1963        // drift on the very first canonical form the two
1964        // implementations disagree on. Byte-shape pins cover the
1965        // ASCII WhatWG-conformant set (space / tab / LF / FF / CR)
1966        // and the strictly-complementary non-ASCII Unicode
1967        // `White_Space` class (NBSP / LINE SEPARATOR / EM-SPACE /
1968        // IDEOGRAPHIC SPACE) on the exemplar `:limits :memory` axis
1969        // — peer of the pre-existing `ser_byte_size_routes_through_
1970        // render_serialize_option_via_str_canonical` /
1971        // `de_byte_size_routes_through_render_deserialize_option_
1972        // via_str_canonical` pins on the sibling codec-hook axis.
1973        for (raw, byte) in [
1974            (" 64MiB", 0x20u8),
1975            ("64MiB ", 0x20u8),
1976            ("64 MiB", 0x20u8),
1977            ("\t64MiB", 0x09u8),
1978            ("64MiB\n", 0x0Au8),
1979        ] {
1980            let err = parse_byte_size(raw)
1981                .expect_err("ASCII-whitespace-carrying byte-size input must be rejected");
1982            let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
1983                raw,
1984                |b| LimitsError::WhitespaceInByteSize {
1985                    value: raw.into(),
1986                    byte: b,
1987                },
1988                |ch| LimitsError::NonAsciiWhitespaceInByteSize {
1989                    value: raw.into(),
1990                    ch,
1991                    codepoint: ch as u32,
1992                },
1993            )
1994            .expect_err("primitive must reject the same ASCII-whitespace shape");
1995            assert_eq!(
1996                err, via_primitive,
1997                "parse_byte_size drifted from crate::render::reject_whitespace \
1998                 on ASCII-whitespace input {raw:?}"
1999            );
2000            assert!(
2001                matches!(
2002                    err,
2003                    LimitsError::WhitespaceInByteSize { value: ref v, byte: b } if v == raw && b == byte
2004                ),
2005                "parse_byte_size must surface WhitespaceInByteSize {{ value: {raw:?}, byte: 0x{byte:02x} }}"
2006            );
2007        }
2008        for (raw, expected_ch) in [
2009            ("\u{00A0}64MiB", '\u{00A0}'),
2010            ("64\u{2003}MiB", '\u{2003}'),
2011            ("64MiB\u{2028}", '\u{2028}'),
2012            ("\u{3000}64MiB", '\u{3000}'),
2013        ] {
2014            let err = parse_byte_size(raw)
2015                .expect_err("non-ASCII-whitespace-carrying byte-size input must be rejected");
2016            let via_primitive = crate::render::reject_whitespace::<LimitsError, _, _>(
2017                raw,
2018                |b| LimitsError::WhitespaceInByteSize {
2019                    value: raw.into(),
2020                    byte: b,
2021                },
2022                |ch| LimitsError::NonAsciiWhitespaceInByteSize {
2023                    value: raw.into(),
2024                    ch,
2025                    codepoint: ch as u32,
2026                },
2027            )
2028            .expect_err("primitive must reject the same non-ASCII-whitespace shape");
2029            assert_eq!(
2030                err, via_primitive,
2031                "parse_byte_size drifted from crate::render::reject_whitespace \
2032                 on non-ASCII-whitespace input {raw:?}"
2033            );
2034            assert!(
2035                matches!(
2036                    err,
2037                    LimitsError::NonAsciiWhitespaceInByteSize { value: ref v, ch, codepoint }
2038                        if v == raw && ch == expected_ch && codepoint == expected_ch as u32
2039                ),
2040                "parse_byte_size must surface NonAsciiWhitespaceInByteSize \
2041                 {{ value: {raw:?}, ch: {expected_ch:?}, codepoint: {cp:#06X} }}",
2042                cp = expected_ch as u32
2043            );
2044        }
2045    }
2046
2047    #[test]
2048    fn limits_round_trip_through_json() {
2049        let limits = LimitsSpec {
2050            memory: Some(64 * 1024 * 1024),
2051            fuel: Some(1_000_000),
2052            wall_clock: Some(Duration::from_secs(30)),
2053            cpu: Some(500),
2054        };
2055        let json = serde_json::to_string(&limits).unwrap();
2056        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2057        assert_eq!(limits, back);
2058    }
2059
2060    #[test]
2061    fn empty_limits_serialises_to_empty_object() {
2062        let limits = LimitsSpec::default();
2063        assert!(limits.is_empty());
2064        let json = serde_json::to_string(&limits).unwrap();
2065        assert_eq!(json, "{}");
2066    }
2067
2068    // ── drift-detection: serde-derive-to-M2_LIMITS_KEY_* identity ────────
2069
2070    #[test]
2071    fn limits_spec_serde_keys_match_lifted_m2_limits_key_consts() {
2072        // Load-bearing invariant: the four `M2_LIMITS_KEY_*` consts
2073        // (`M2_LIMITS_KEY_MEMORY` / `M2_LIMITS_KEY_FUEL` /
2074        // `M2_LIMITS_KEY_WALL_CLOCK` / `M2_LIMITS_KEY_CPU`) name the
2075        // exact camelCase JSON keys the `#[serde(rename_all = "camelCase")]`
2076        // attribute on `LimitsSpec` emits, and every test-side probe
2077        // across the caixa-core / caixa-flux / caixa-helm renderer test
2078        // fixtures navigates into the rendered `:limits` overlay
2079        // sub-block by consulting one of these four `&'static str`s.
2080        // Serialize a fully-populated LimitsSpec and pin that each
2081        // canonical byte-sequence appears verbatim in the JSON — a
2082        // future accidental `rename_all = "snake_case"` /
2083        // `"kebab-case"` / verbatim-field-name flip at the derive
2084        // attribute (any of which would silently break every test-side
2085        // probe that reaches for one of the four consts) surfaces here
2086        // as a build-time test failure at `limits.rs`, not as an
2087        // apply-time `.get(<stale-canonical-const>)` returning `None`
2088        // far from the derive-attr drift's commit. Same discipline the
2089        // sibling M3 `PlacementStrategy::as_str` lift (0a2f653)
2090        // established on the peer per-`:placement :estrategia` axis:
2091        // one canonical byte-string per typed sub-key axis, pinned to
2092        // the load-bearing serde derivation at the type itself.
2093        let limits = LimitsSpec {
2094            memory: Some(64 * 1024 * 1024),
2095            fuel: Some(1_000_000),
2096            wall_clock: Some(Duration::from_secs(30)),
2097            cpu: Some(500),
2098        };
2099        let json = serde_json::to_string(&limits).unwrap();
2100        for key in [
2101            crate::render::M2_LIMITS_KEY_MEMORY,
2102            crate::render::M2_LIMITS_KEY_FUEL,
2103            crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2104            crate::render::M2_LIMITS_KEY_CPU,
2105        ] {
2106            let quoted = format!("\"{key}\"");
2107            assert!(
2108                json.contains(&quoted),
2109                "serialized LimitsSpec must carry the lifted \
2110                 M2_LIMITS_KEY_* byte-sequence {quoted} verbatim in \
2111                 the JSON emission (got: {json})",
2112            );
2113        }
2114    }
2115
2116    #[test]
2117    fn m2_limits_key_consts_are_pairwise_distinct() {
2118        // Cross-axis drift-detection pin: a future collapse of two
2119        // canonical sub-key byte-strings onto the same value (e.g. an
2120        // accidental copy-paste flip of `M2_LIMITS_KEY_CPU` to also
2121        // read `"memory"`) would silently reroute every test-side
2122        // probe on one axis onto the sibling axis's overlay entry and
2123        // pass every propagation-probe test that expected only the
2124        // stale axis's value. Peer of the sibling three-way distinct
2125        // pin on the `FLUX_GITREPOSITORY_REF_KEY_*` trio (7d40380).
2126        let all = [
2127            crate::render::M2_LIMITS_KEY_MEMORY,
2128            crate::render::M2_LIMITS_KEY_FUEL,
2129            crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2130            crate::render::M2_LIMITS_KEY_CPU,
2131        ];
2132        for (i, a) in all.iter().enumerate() {
2133            for b in all.iter().skip(i + 1) {
2134                assert_ne!(
2135                    a, b,
2136                    "M2_LIMITS_KEY_* consts must be pairwise-distinct \
2137                     canonical byte-sequences — got `{a}` == `{b}`",
2138                );
2139            }
2140        }
2141    }
2142
2143    #[test]
2144    fn m2_limits_key_consts_are_lower_camel_case_shape() {
2145        // Shape-pin: every `M2_LIMITS_KEY_*` const must be a
2146        // lowerCamelCase byte-sequence (no `snake_case` underscores,
2147        // no `kebab-case` hyphens, no `PascalCase` leading capital, no
2148        // whitespace / colons / dots) — the canonical shape the
2149        // `#[serde(rename_all = "camelCase")]` derive produces on
2150        // `LimitsSpec`. A future flip to a non-camelCase attribute at
2151        // the derive surfaces both here (this test fails on the
2152        // stale-constant shape) and at
2153        // `limits_spec_serde_keys_match_lifted_m2_limits_key_consts`
2154        // (that test fails on the mismatch between const and derive).
2155        for key in [
2156            crate::render::M2_LIMITS_KEY_MEMORY,
2157            crate::render::M2_LIMITS_KEY_FUEL,
2158            crate::render::M2_LIMITS_KEY_WALL_CLOCK,
2159            crate::render::M2_LIMITS_KEY_CPU,
2160        ] {
2161            assert!(
2162                !key.is_empty(),
2163                "M2_LIMITS_KEY_* must be non-empty (got {key:?})"
2164            );
2165            let first = key.chars().next().unwrap();
2166            assert!(
2167                first.is_ascii_lowercase(),
2168                "M2_LIMITS_KEY_* must lead with an ASCII-lowercase byte \
2169                 (got {key:?}, leads with {first:?})",
2170            );
2171            assert!(
2172                key.chars().all(|c| c.is_ascii_alphanumeric()),
2173                "M2_LIMITS_KEY_* must be ASCII-alphanumeric only \
2174                 — no `_` / `-` / `:` / `.` / whitespace (got {key:?})",
2175            );
2176        }
2177    }
2178
2179    // ── value-shape: zero on any declared axis is rejected ────────────────
2180
2181    #[test]
2182    fn validate_accepts_default_unbounded_limits() {
2183        // Every axis None → "no bound declared" is the omit-the-slot
2184        // shape and stays valid. This is the pre-M2 default behaviour.
2185        LimitsSpec::default().validate().unwrap();
2186    }
2187
2188    #[test]
2189    fn validate_accepts_full_nonzero_limits() {
2190        let l = LimitsSpec {
2191            memory: Some(64 * 1024 * 1024),
2192            fuel: Some(1_000_000),
2193            wall_clock: Some(Duration::from_secs(30)),
2194            cpu: Some(500),
2195        };
2196        l.validate().unwrap();
2197    }
2198
2199    #[test]
2200    fn validate_rejects_zero_memory() {
2201        let l = LimitsSpec {
2202            memory: Some(0),
2203            ..Default::default()
2204        };
2205        assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2206    }
2207
2208    #[test]
2209    fn validate_rejects_zero_fuel() {
2210        let l = LimitsSpec {
2211            fuel: Some(0),
2212            ..Default::default()
2213        };
2214        assert_eq!(l.validate().unwrap_err(), LimitsError::FuelZero);
2215    }
2216
2217    #[test]
2218    fn validate_rejects_zero_wall_clock() {
2219        let l = LimitsSpec {
2220            wall_clock: Some(Duration::ZERO),
2221            ..Default::default()
2222        };
2223        assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
2224    }
2225
2226    #[test]
2227    fn validate_rejects_zero_cpu() {
2228        let l = LimitsSpec {
2229            cpu: Some(0),
2230            ..Default::default()
2231        };
2232        assert_eq!(l.validate().unwrap_err(), LimitsError::CpuZero);
2233    }
2234
2235    #[test]
2236    fn validate_rejects_first_zero_axis_deterministically() {
2237        // Memory is checked first; with multiple zero axes, the
2238        // diagnostic names :memory rather than reporting some other
2239        // axis non-deterministically.
2240        let l = LimitsSpec {
2241            memory: Some(0),
2242            fuel: Some(0),
2243            wall_clock: Some(Duration::ZERO),
2244            cpu: Some(0),
2245        };
2246        assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2247    }
2248
2249    // ── value-shape: :memory upper bound — wasm32-wasip2 4 GiB ceiling ────
2250
2251    #[test]
2252    fn wasm32_memory_cap_matches_parsed_4_gib() {
2253        // The cap constant tracks the canonical "4 GiB" byte-size
2254        // codec output structurally — drift between the codec's
2255        // accepted magnitude for `"4GiB"` and the validate gate's
2256        // accepted upper bound would surface here, not as a silent
2257        // round-trip break at the renderer layer. Same single-source-
2258        // of-truth shape the is_canonical_rate_limit_window predicate
2259        // gives the rate-limit window set.
2260        assert_eq!(
2261            parse_byte_size("4GiB").unwrap(),
2262            LIMITS_MEMORY_WASM32_MAX_BYTES
2263        );
2264        assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 4 * 1024 * 1024 * 1024);
2265        assert_eq!(LIMITS_MEMORY_WASM32_MAX_BYTES, 1u64 << 32);
2266    }
2267
2268    #[test]
2269    fn validate_accepts_memory_at_wasm32_cap() {
2270        // 4 GiB exactly is the wasm32 in-spec maximum — `2^16 pages ×
2271        // 2^16 bytes/page`. The validate gate is inclusive on the
2272        // upper end (mirrors the inclusive lower-end rejection: zero
2273        // is *out*, one is *in*; 4 GiB+1 is *out*, 4 GiB is *in*).
2274        let l = LimitsSpec {
2275            memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
2276            ..Default::default()
2277        };
2278        l.validate().unwrap();
2279    }
2280
2281    #[test]
2282    fn validate_rejects_memory_one_byte_above_wasm32_cap() {
2283        // Boundary case: exactly 1 byte past the cap. Catches a
2284        // future "strictly less than" half-measure and pins the
2285        // diagnostic to name the offending byte count verbatim.
2286        let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
2287        let l = LimitsSpec {
2288            memory: Some(bytes),
2289            ..Default::default()
2290        };
2291        assert_eq!(
2292            l.validate().unwrap_err(),
2293            LimitsError::MemoryExceedsWasm32Cap { bytes }
2294        );
2295    }
2296
2297    #[test]
2298    fn validate_rejects_memory_8_gib() {
2299        // The "obvious authoring footgun" case: a value the byte-size
2300        // codec accepts cleanly (`"8GiB"` → 8 * 1024^3 bytes) and
2301        // serde round-trips silently, but no wasm32 component can
2302        // honor. Until this gate landed `validate` accepted it.
2303        let bytes = parse_byte_size("8GiB").unwrap();
2304        let l = LimitsSpec {
2305            memory: Some(bytes),
2306            ..Default::default()
2307        };
2308        assert_eq!(
2309            l.validate().unwrap_err(),
2310            LimitsError::MemoryExceedsWasm32Cap { bytes }
2311        );
2312    }
2313
2314    #[test]
2315    fn validate_memory_zero_takes_precedence_over_cap_check() {
2316        // Memory zero is structurally meaningless under *any* wasm
2317        // engine (zero-cap traps the first allocation); above-cap is
2318        // wasm32-specific. The zero arm fires first so the canonical
2319        // "omit the slot for unbounded" remediation in the existing
2320        // MemoryZero diagnostic still leads — pinning this precedence
2321        // guards against a future re-ordering that would surface the
2322        // wasm32-specific message in the case where the simpler
2323        // zero-floor message is more actionable.
2324        let l = LimitsSpec {
2325            memory: Some(0),
2326            ..Default::default()
2327        };
2328        assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2329    }
2330
2331    #[test]
2332    fn validate_rejects_memory_cap_before_other_axes() {
2333        // With both an above-cap :memory and a zero :fuel, the
2334        // diagnostic names :memory rather than :fuel — peer of the
2335        // existing `validate_rejects_first_zero_axis_deterministically`
2336        // ordering pin.
2337        let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1024;
2338        let l = LimitsSpec {
2339            memory: Some(bytes),
2340            fuel: Some(0),
2341            wall_clock: Some(Duration::ZERO),
2342            cpu: Some(0),
2343        };
2344        assert_eq!(
2345            l.validate().unwrap_err(),
2346            LimitsError::MemoryExceedsWasm32Cap { bytes }
2347        );
2348    }
2349
2350    #[test]
2351    fn above_cap_value_still_round_trips_through_serde() {
2352        // The byte-size codec accepts the above-cap value (the cap
2353        // lives in the validate gate, not the codec). This pins that
2354        // the structural property is "above-cap is rejected by
2355        // validate" — not "above-cap is unparseable by the codec";
2356        // the latter would prevent the diagnostic from naming the
2357        // offending byte count at all, since deserialize would fail
2358        // first.
2359        let l = LimitsSpec {
2360            memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
2361            ..Default::default()
2362        };
2363        let json = serde_json::to_string(&l).unwrap();
2364        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2365        assert_eq!(l, back);
2366        assert!(back.validate().is_err());
2367    }
2368
2369    // ── value-shape: :memory lower bound — wasm32-wasip2 64 KiB page floor ─
2370
2371    #[test]
2372    fn wasm32_memory_page_matches_parsed_64_kib() {
2373        // The page-floor constant tracks the canonical "64 KiB"
2374        // byte-size codec output structurally — drift between the
2375        // codec's accepted magnitude for `"64KiB"` and the validate
2376        // gate's accepted lower bound would surface here, not as a
2377        // silent round-trip break at the renderer layer. Same single-
2378        // source-of-truth shape `wasm32_memory_cap_matches_parsed_4_gib`
2379        // pins on the peer upper-cap bound and
2380        // `is_canonical_rate_limit_window` gives the rate-limit window
2381        // set. The page-size identities (2^16, integer-divides the
2382        // upper cap exactly 2^16 times) are pinned alongside so a
2383        // future memory64-target opt-in raising one bound surfaces
2384        // here if the other bound's relationship to it drifts.
2385        assert_eq!(
2386            parse_byte_size("64KiB").unwrap(),
2387            LIMITS_MEMORY_WASM32_PAGE_BYTES
2388        );
2389        assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 64 * 1024);
2390        assert_eq!(LIMITS_MEMORY_WASM32_PAGE_BYTES, 1u64 << 16);
2391        assert_eq!(
2392            LIMITS_MEMORY_WASM32_MAX_BYTES / LIMITS_MEMORY_WASM32_PAGE_BYTES,
2393            1u64 << 16,
2394            "the wasm32 page count cap is 2^16 pages exactly",
2395        );
2396        assert_eq!(
2397            LIMITS_MEMORY_WASM32_MAX_BYTES % LIMITS_MEMORY_WASM32_PAGE_BYTES,
2398            0
2399        );
2400    }
2401
2402    #[test]
2403    fn validate_rejects_memory_below_wasm32_page() {
2404        // The fail-before-pass-after pin: until this gate landed a
2405        // `(:memory "32KiB")` (or any programmatic struct literal with
2406        // a sub-page byte count — `LimitsSpec { memory: Some(50000),
2407        // .. }`) silently passed validate, the byte-size codec
2408        // round-tripped cleanly through serde, and the wasm-engine
2409        // either refused instantiation (`memory minimum size of 1
2410        // pages exceeds memory limits` on any cdylib-shaped component
2411        // declaring `(memory 1)`) or trapped the first `memory.grow(1)`
2412        // far from the source caixa.lisp.
2413        let bytes = parse_byte_size("32KiB").unwrap();
2414        let l = LimitsSpec {
2415            memory: Some(bytes),
2416            ..Default::default()
2417        };
2418        assert_eq!(
2419            l.validate().unwrap_err(),
2420            LimitsError::MemoryBelowWasm32Page { bytes }
2421        );
2422    }
2423
2424    #[test]
2425    fn validate_rejects_memory_one_byte_below_page() {
2426        // Boundary case: exactly 1 byte below the page-size floor
2427        // (`LIMITS_MEMORY_WASM32_PAGE_BYTES - 1` = 65535 bytes). Pins
2428        // the inclusive-upper-end / strict-lower-end relationship on
2429        // the page-floor arm: 65535 is *out*, 65536 is *in*. Catches a
2430        // future "strictly greater than" half-measure and matches the
2431        // peer `validate_rejects_memory_one_byte_above_wasm32_cap`
2432        // shape on the top edge.
2433        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
2434        let l = LimitsSpec {
2435            memory: Some(bytes),
2436            ..Default::default()
2437        };
2438        assert_eq!(
2439            l.validate().unwrap_err(),
2440            LimitsError::MemoryBelowWasm32Page { bytes }
2441        );
2442    }
2443
2444    #[test]
2445    fn validate_rejects_memory_one_byte() {
2446        // The far-floor case: a `(:memory "1")` cap is non-zero (so
2447        // `MemoryZero` doesn't fire) but structurally cannot hold any
2448        // wasm linear memory page. The page-floor gate at this layer
2449        // surfaces a self-locating diagnostic naming the offending
2450        // byte count verbatim rather than a downstream wasm-engine
2451        // instantiation failure whose error message points at the
2452        // engine's internals, not the caixa.lisp `:memory` slot.
2453        let l = LimitsSpec {
2454            memory: Some(1),
2455            ..Default::default()
2456        };
2457        assert_eq!(
2458            l.validate().unwrap_err(),
2459            LimitsError::MemoryBelowWasm32Page { bytes: 1 }
2460        );
2461    }
2462
2463    #[test]
2464    fn validate_accepts_memory_at_wasm32_page() {
2465        // 64 KiB exactly is the wasm32 linear-memory page size — the
2466        // smallest cap that admits one wasm `(memory 1)` page. The
2467        // page-floor gate is inclusive on the lower end (mirrors the
2468        // inclusive upper-end acceptance: 4 GiB is *in*, 4 GiB+1 is
2469        // *out*; 64 KiB is *in*, 64 KiB-1 is *out*).
2470        let l = LimitsSpec {
2471            memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
2472            ..Default::default()
2473        };
2474        l.validate().unwrap();
2475    }
2476
2477    #[test]
2478    fn validate_accepts_multi_page_memory() {
2479        // The positive-control sweep: every typed `:memory` cap that
2480        // admits at least one wasm linear memory page (i.e. ≥
2481        // `LIMITS_MEMORY_WASM32_PAGE_BYTES`) passes `validate`. Sweeps
2482        // single-page, two-page, the canonical 64 MiB / 1 GiB / 4 GiB
2483        // upper-bound boundary so a future tightening of either edge
2484        // surfaces here. Peer of
2485        // `validate_accepts_integer_millisecond_wall_clock_values` on
2486        // the sibling `:wall-clock` axis.
2487        for bytes in [
2488            LIMITS_MEMORY_WASM32_PAGE_BYTES,
2489            2 * LIMITS_MEMORY_WASM32_PAGE_BYTES,
2490            64 * 1024 * 1024,
2491            1024 * 1024 * 1024,
2492            LIMITS_MEMORY_WASM32_MAX_BYTES,
2493        ] {
2494            let l = LimitsSpec {
2495                memory: Some(bytes),
2496                ..Default::default()
2497            };
2498            l.validate()
2499                .unwrap_or_else(|e| panic!("multi-page {bytes} must validate, got {e:?}"));
2500        }
2501    }
2502
2503    #[test]
2504    fn validate_memory_zero_takes_precedence_over_page_floor() {
2505        // Cross-arm ordering pin: `Some(0)` would otherwise pass the
2506        // page-floor arm's `m < PAGE_BYTES` check (0 < 65536), but the
2507        // zero-floor arm strictly precedes the page-floor arm so the
2508        // more self-locating `MemoryZero` diagnostic (with its omit-
2509        // axis remediation directly named, applicable under *any* wasm
2510        // engine not just wasm32) leads. Same posture every peer
2511        // zero-then-shape gate uses on this surface
2512        // (`PolicyTimeoutZero` → `PolicyTimeoutNotCanonical`,
2513        // `PolicyBreakerZeroWindow` → `PolicyBreakerWindowNotCanonical`,
2514        // `WallClockZero` → `WallClockNotCanonical`).
2515        let l = LimitsSpec {
2516            memory: Some(0),
2517            ..Default::default()
2518        };
2519        assert_eq!(l.validate().unwrap_err(), LimitsError::MemoryZero);
2520    }
2521
2522    #[test]
2523    fn validate_memory_page_floor_takes_precedence_over_other_axes() {
2524        // With a sub-page `:memory` and zero values on every other
2525        // axis, the diagnostic names `:memory` rather than `:fuel` /
2526        // `:wall-clock` / `:cpu` — peer of the existing
2527        // `validate_rejects_first_zero_axis_deterministically` and
2528        // `validate_rejects_memory_cap_before_other_axes` ordering
2529        // pins. Memory is the first axis the validate cascade checks,
2530        // so a sub-page value surfaces before any other-axis
2531        // diagnostic regardless of how many other axes are
2532        // simultaneously invalid.
2533        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES / 2;
2534        let l = LimitsSpec {
2535            memory: Some(bytes),
2536            fuel: Some(0),
2537            wall_clock: Some(Duration::ZERO),
2538            cpu: Some(0),
2539        };
2540        assert_eq!(
2541            l.validate().unwrap_err(),
2542            LimitsError::MemoryBelowWasm32Page { bytes }
2543        );
2544    }
2545
2546    #[test]
2547    fn memory_page_floor_diagnostic_carries_offending_bytes() {
2548        // Diagnostic-shape pin: the page-floor arm names the
2549        // offending byte count verbatim so the author's grep lands on
2550        // the field's value, not a generic "memory too small" message.
2551        // Same shape every other typed-cap arm on this surface
2552        // carries (`MemoryExceedsWasm32Cap` carries the offending byte
2553        // count verbatim, `WallClockNotCanonical` carries the
2554        // offending `Duration` verbatim, `PolicyRetriesExceedsCap`
2555        // carries the offending retry count verbatim).
2556        let l = LimitsSpec {
2557            memory: Some(50_000),
2558            ..Default::default()
2559        };
2560        let err = l.validate().unwrap_err();
2561        let msg = err.to_string();
2562        assert!(
2563            msg.contains("50000"),
2564            "diagnostic must carry the offending byte count verbatim (got {msg:?})"
2565        );
2566        assert!(
2567            msg.contains("64 KiB") || msg.contains("65536"),
2568            "diagnostic must name the page-size floor (got {msg:?})"
2569        );
2570    }
2571
2572    #[test]
2573    fn below_page_value_still_round_trips_through_serde() {
2574        // The byte-size codec accepts the sub-page value (the floor
2575        // lives in the validate gate, not the codec) — peer of
2576        // `above_cap_value_still_round_trips_through_serde` on the top
2577        // edge. Pins that the structural property is "sub-page is
2578        // rejected by validate" — not "sub-page is unparseable by the
2579        // codec"; the latter would prevent the diagnostic from naming
2580        // the offending byte count at all, since deserialize would
2581        // fail first.
2582        let l = LimitsSpec {
2583            memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1),
2584            ..Default::default()
2585        };
2586        let json = serde_json::to_string(&l).unwrap();
2587        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2588        assert_eq!(l, back);
2589        assert!(back.validate().is_err());
2590    }
2591
2592    // ── value-shape: :memory page-multiple granularity gate ───────────────
2593
2594    #[test]
2595    fn validate_rejects_memory_one_byte_above_page() {
2596        // The fail-before-pass-after pin: until this gate landed a
2597        // `LimitsSpec { memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES +
2598        // 1), .. }` (65537 bytes — one wasm32 page plus a 1-byte
2599        // unreachable residue) silently passed validate, the byte-size
2600        // codec round-tripped cleanly through serde (`render_byte_size`
2601        // falls through to `"65537"` on any non-power-of-1024 magnitude),
2602        // and wasmtime's `StoreLimits::memory_size` consumed the value
2603        // verbatim as a page-quantized ceiling — the engine grew at
2604        // most floor(65537 / 65536) = 1 page, and the byte at offset
2605        // 65536 became structural dead space the runtime cannot honor.
2606        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
2607        let l = LimitsSpec {
2608            memory: Some(bytes),
2609            ..Default::default()
2610        };
2611        assert_eq!(
2612            l.validate().unwrap_err(),
2613            LimitsError::MemoryNotPageMultiple { bytes }
2614        );
2615    }
2616
2617    #[test]
2618    fn validate_rejects_memory_just_below_two_pages() {
2619        // Boundary case: exactly 1 byte below two pages (`2 *
2620        // LIMITS_MEMORY_WASM32_PAGE_BYTES - 1` = 131071 bytes). Pins
2621        // the inclusive-page-boundary / strict-sub-page-residue
2622        // relationship on the page-multiple arm: 131071 is *out*
2623        // (sub-page residue), 131072 is *in* (exactly two pages).
2624        // Matches the peer `validate_rejects_memory_one_byte_below_page`
2625        // / `validate_rejects_memory_one_byte_above_wasm32_cap` shape
2626        // on the surrounding edges.
2627        let bytes = 2 * LIMITS_MEMORY_WASM32_PAGE_BYTES - 1;
2628        let l = LimitsSpec {
2629            memory: Some(bytes),
2630            ..Default::default()
2631        };
2632        assert_eq!(
2633            l.validate().unwrap_err(),
2634            LimitsError::MemoryNotPageMultiple { bytes }
2635        );
2636    }
2637
2638    #[test]
2639    fn validate_rejects_memory_100000_bytes() {
2640        // The "obvious authoring footgun" case: a magnitude the
2641        // byte-size codec accepts cleanly (`"100000"` → 100000 bytes
2642        // ≈ 97.65 KiB) and serde round-trips silently, but no wasm32
2643        // engine can honor as a meaningful ceiling — the engine grows
2644        // at most floor(100000 / 65536) = 1 page, and the 34464 bytes
2645        // between offsets 65536 and 100000 are structural dead space.
2646        // Until this gate landed `validate` accepted it. Peer of
2647        // `validate_rejects_memory_8_gib` on the cap arm.
2648        let bytes = parse_byte_size("100000").unwrap();
2649        let l = LimitsSpec {
2650            memory: Some(bytes),
2651            ..Default::default()
2652        };
2653        assert_eq!(
2654            l.validate().unwrap_err(),
2655            LimitsError::MemoryNotPageMultiple { bytes }
2656        );
2657    }
2658
2659    #[test]
2660    fn validate_accepts_every_page_aligned_value_through_serde() {
2661        // Positive-control sweep through the byte-size codec: every
2662        // canonical magnitude `render_byte_size` emits at or above
2663        // the page floor divides cleanly by the page size, so the
2664        // page-multiple gate accepts the entire canonical-output
2665        // domain at and above the page floor. The sweep walks
2666        // single-page (`"64KiB"`), two-page (`"128KiB"`), every
2667        // power-of-1024 unit (`"1MiB"`, `"64MiB"`, `"1GiB"`, `"4GiB"`),
2668        // and the cap (`"4GiB"`) — pinning that the codec's
2669        // emitted-canonical-form set is a structural subset of the
2670        // validate gate's accepted set. Drift between the codec's
2671        // emit alphabet and the validate gate would surface here
2672        // rather than at a future serializer round trip.
2673        for s in ["64KiB", "128KiB", "1MiB", "64MiB", "1GiB", "4GiB"] {
2674            let bytes = parse_byte_size(s).unwrap();
2675            assert_eq!(
2676                bytes % LIMITS_MEMORY_WASM32_PAGE_BYTES,
2677                0,
2678                "canonical byte-size codec output {s:?} ({bytes}) must be page-aligned",
2679            );
2680            let l = LimitsSpec {
2681                memory: Some(bytes),
2682                ..Default::default()
2683            };
2684            l.validate()
2685                .unwrap_or_else(|e| panic!("canonical {s:?} = {bytes} must validate, got {e:?}"));
2686        }
2687    }
2688
2689    #[test]
2690    fn validate_memory_below_page_takes_precedence_over_page_multiple() {
2691        // Cross-arm ordering pin: `Some(1)` would otherwise pass the
2692        // page-multiple arm's `m % PAGE_BYTES != 0` check (1 % 65536
2693        // == 1 ≠ 0), but the page-floor arm strictly precedes the
2694        // page-multiple arm so the more self-locating
2695        // `MemoryBelowWasm32Page` diagnostic (with its "single page
2696        // cannot fit" remediation, applicable to every sub-page
2697        // value uniformly) leads. Peer of `MemoryZero` →
2698        // `MemoryBelowWasm32Page` precedence on the zero edge:
2699        // every value `m` in the range `1..=PAGE_BYTES-1` satisfies
2700        // both `m < PAGE_BYTES` and `m % PAGE_BYTES != 0`, but the
2701        // structurally-narrower diagnostic (page-floor) leads.
2702        let l = LimitsSpec {
2703            memory: Some(1),
2704            ..Default::default()
2705        };
2706        assert_eq!(
2707            l.validate().unwrap_err(),
2708            LimitsError::MemoryBelowWasm32Page { bytes: 1 }
2709        );
2710    }
2711
2712    #[test]
2713    fn validate_memory_cap_takes_precedence_over_page_multiple() {
2714        // Cross-arm ordering pin: `LIMITS_MEMORY_WASM32_MAX_BYTES + 1`
2715        // (4 GiB + 1 byte) is *both* above-cap and not page-aligned.
2716        // The cap arm strictly precedes the page-multiple arm so the
2717        // more aggressive cap-shape diagnostic leads (the page-multiple
2718        // remediation would be misleading when the offending value
2719        // exceeds the wasm32 address-space ceiling anyway — the
2720        // canonical fix collapses both into "pin a page-aligned value
2721        // ≤ 4 GiB"). Peer of `WallClockNotCanonical` →
2722        // `WallClockExceedsCap` ordering on the sibling `:wall-clock`
2723        // axis (with the inverse polarity — there the granularity
2724        // gate leads because sub-millisecond residue breaks serde
2725        // round-trip; here the cap leads because both gates' offending
2726        // values round-trip cleanly through serde and the broader
2727        // magnitude constraint is the more aggressive one).
2728        let bytes = LIMITS_MEMORY_WASM32_MAX_BYTES + 1;
2729        let l = LimitsSpec {
2730            memory: Some(bytes),
2731            ..Default::default()
2732        };
2733        assert_eq!(
2734            l.validate().unwrap_err(),
2735            LimitsError::MemoryExceedsWasm32Cap { bytes }
2736        );
2737    }
2738
2739    #[test]
2740    fn validate_rejects_memory_page_multiple_before_other_axes() {
2741        // With a sub-page-residue `:memory` and zero values on every
2742        // other axis, the diagnostic names `:memory` rather than
2743        // `:fuel` / `:wall-clock` / `:cpu` — peer of the existing
2744        // `validate_memory_page_floor_takes_precedence_over_other_axes`
2745        // and `validate_rejects_memory_cap_before_other_axes` ordering
2746        // pins. Memory is the first axis the validate cascade checks,
2747        // so a sub-page-residue value surfaces before any other-axis
2748        // diagnostic regardless of how many other axes are
2749        // simultaneously invalid.
2750        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
2751        let l = LimitsSpec {
2752            memory: Some(bytes),
2753            fuel: Some(0),
2754            wall_clock: Some(Duration::ZERO),
2755            cpu: Some(0),
2756        };
2757        assert_eq!(
2758            l.validate().unwrap_err(),
2759            LimitsError::MemoryNotPageMultiple { bytes }
2760        );
2761    }
2762
2763    #[test]
2764    fn memory_page_multiple_diagnostic_carries_offending_bytes() {
2765        // Diagnostic-shape pin: the page-multiple arm names the
2766        // offending byte count verbatim so the author's grep lands on
2767        // the field's value, not a generic "memory not aligned"
2768        // message. Same shape every other typed-cap arm on this
2769        // surface carries (`MemoryExceedsWasm32Cap` carries the
2770        // offending byte count verbatim, `WallClockNotCanonical`
2771        // carries the offending `Duration` verbatim).
2772        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 12345;
2773        let l = LimitsSpec {
2774            memory: Some(bytes),
2775            ..Default::default()
2776        };
2777        let err = l.validate().unwrap_err();
2778        let msg = err.to_string();
2779        assert!(
2780            msg.contains(&bytes.to_string()),
2781            "diagnostic must carry the offending byte count verbatim (got {msg:?})"
2782        );
2783        assert!(
2784            msg.contains("64 KiB") || msg.contains("65536") || msg.contains("page"),
2785            "diagnostic must name the page-size granularity (got {msg:?})"
2786        );
2787    }
2788
2789    #[test]
2790    fn sub_page_residue_value_still_round_trips_through_serde() {
2791        // The byte-size codec accepts the sub-page-residue value (the
2792        // page-multiple gate lives in validate, not in the codec) —
2793        // peer of `above_cap_value_still_round_trips_through_serde`
2794        // and `below_page_value_still_round_trips_through_serde`.
2795        // Pins that the structural property is "sub-page-residue is
2796        // rejected by validate" — not "sub-page-residue is
2797        // unparseable by the codec"; the latter would prevent the
2798        // diagnostic from naming the offending byte count at all,
2799        // since deserialize would fail first. The render-then-parse
2800        // round trip also pins the codec's flow-through-to-bytes
2801        // shape on non-power-of-1024 magnitudes: `render_byte_size`
2802        // falls through every `(mult, label)` arm whose `n % mult !=
2803        // 0` and emits the bare byte count.
2804        let bytes = LIMITS_MEMORY_WASM32_PAGE_BYTES + 1;
2805        let l = LimitsSpec {
2806            memory: Some(bytes),
2807            ..Default::default()
2808        };
2809        let json = serde_json::to_string(&l).unwrap();
2810        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
2811        assert_eq!(l, back);
2812        assert!(back.validate().is_err());
2813    }
2814
2815    #[test]
2816    fn validate_memory_axis_routes_through_quantum_multiple_bounded_helper() {
2817        // Byte-parity pin on the pre-lift `if self.memory() == Some(0)
2818        // { … } if let Some(m) = self.memory() { if m <
2819        // LIMITS_MEMORY_WASM32_PAGE_BYTES { … } } if let Some(m) =
2820        // self.memory() { if m > LIMITS_MEMORY_WASM32_MAX_BYTES { … } }
2821        // if let Some(m) = self.memory() && m %
2822        // LIMITS_MEMORY_WASM32_PAGE_BYTES != 0 { … }` four-sequential-
2823        // `if let` shape the `LimitsSpec::validate` `:memory` axis
2824        // routed through today via
2825        // `crate::render::require_positive_quantum_multiple_bounded_u64`.
2826        // Refuses a future accidental split between the helper's
2827        // four-arm ordering (zero → below-quantum → cap → not-multiple)
2828        // and the four typed `LimitsError::Memory*` variants each arm
2829        // threads its offending byte count into — a swap of any two
2830        // arms in the helper, or a partial widening (e.g. removing the
2831        // page-multiple arm), or a widening of the `on_below_quantum`
2832        // arm's closure to the `MemoryExceedsWasm32Cap` variant instead
2833        // of `MemoryBelowWasm32Page` — would break exactly one row of
2834        // this pin, matching the pre-lift shape the four consumer sites
2835        // route through today. Same shape as
2836        // `as_seq_body_partitions_the_same_arm_set_as_seq_delims` in
2837        // caixa-ast and the peer `require_positive_bounded_u64` tests
2838        // in the sibling render.rs test module.
2839        //
2840        // (Some(bytes) → expected LimitsError)
2841        let quantum = LIMITS_MEMORY_WASM32_PAGE_BYTES;
2842        let cap = LIMITS_MEMORY_WASM32_MAX_BYTES;
2843        let cases: &[(u64, LimitsError)] = &[
2844            (0, LimitsError::MemoryZero),
2845            (1, LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
2846            (
2847                quantum - 1,
2848                LimitsError::MemoryBelowWasm32Page { bytes: quantum - 1 },
2849            ),
2850            (
2851                cap + 1,
2852                LimitsError::MemoryExceedsWasm32Cap { bytes: cap + 1 },
2853            ),
2854            (
2855                cap + quantum,
2856                LimitsError::MemoryExceedsWasm32Cap {
2857                    bytes: cap + quantum,
2858                },
2859            ),
2860            (
2861                quantum + 1,
2862                LimitsError::MemoryNotPageMultiple { bytes: quantum + 1 },
2863            ),
2864            (
2865                quantum + 12_345,
2866                LimitsError::MemoryNotPageMultiple {
2867                    bytes: quantum + 12_345,
2868                },
2869            ),
2870        ];
2871        for (bytes, expected) in cases {
2872            let l = LimitsSpec {
2873                memory: Some(*bytes),
2874                ..Default::default()
2875            };
2876            assert_eq!(
2877                l.validate().unwrap_err(),
2878                *expected,
2879                "memory={bytes} must surface the {expected:?} arm via the substrate helper",
2880            );
2881        }
2882        // Positive-control: every quantum-multiple in `quantum..=cap`
2883        // passes, closing the four-arm cascade with an `Ok(())` shape.
2884        for bytes in [quantum, quantum * 2, quantum * 100, cap] {
2885            let l = LimitsSpec {
2886                memory: Some(bytes),
2887                ..Default::default()
2888            };
2889            l.validate().unwrap();
2890        }
2891    }
2892
2893    // ── canonical-form: integer-magnitude byte-size codec gate ────────────
2894    //
2895    // Every magnitude `render_byte_size` emits is a non-negative integer
2896    // (no decimal point, no leading sign, no scientific notation). The
2897    // parser's accepted set must match for parse → render → parse to
2898    // round-trip without canonical-form drift. The tests below pin every
2899    // canonical-drift shape — fractional (`"1.5KiB"`), decimal-shaped-
2900    // integer (`"1.0MiB"`), half-unit (`"0.5GiB"`), leading-`+`
2901    // (`"+1024"`) — plus the scientific-notation dispatch path (caught
2902    // by `UnknownByteUnit` on a different arm), the two complement-side
2903    // pins (the integer happy paths the gate must continue to accept),
2904    // the round-trip convergence property (parse → render → parse must
2905    // converge on a single canonical form for every accepted input),
2906    // the BadByteMagnitude-precedence pin (genuinely unparseable inputs
2907    // keep their narrower diagnostic), the overflow-surface pin
2908    // (u64-overflow on magnitude × unit surfaces at parse time), and
2909    // the serde-path pin (the gate fires at deserialize, before any
2910    // validate gate runs).
2911
2912    #[test]
2913    fn parse_byte_size_rejects_fractional_kib() {
2914        // The fail-before-pass-after pin: `"1.5KiB"` parsed cleanly on
2915        // every pre-gate codebase (f64::parse accepts the decimal), the
2916        // codec produced 1536 bytes, and `render_byte_size(1536)`
2917        // emitted `"1536"` on the next serialize — silently drifting
2918        // the canonical form away from the author's intent. The new
2919        // gate surfaces the round-trip break at the parser layer with
2920        // a self-locating diagnostic (the offending magnitude verbatim,
2921        // the canonical-form remediation in the wording).
2922        let err = parse_byte_size("1.5KiB").unwrap_err();
2923        assert!(
2924            matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.5"),
2925            "got {err:?}"
2926        );
2927    }
2928
2929    #[test]
2930    fn parse_byte_size_rejects_decimal_shaped_integer() {
2931        // The canonical-drift case where the *value* is integer but
2932        // the *form* carries a redundant decimal point — `"1.0MiB"`
2933        // parses to 1 MiB (integer), but the renderer emits `"1MiB"`
2934        // on the next serialize (no decimal point). The parse-shape
2935        // gate fires here too so the codec's accepted set is exactly
2936        // the renderer's emitted set — no `"1.0MiB"` ↔ `"1MiB"` drift
2937        // surviving a round-trip silently.
2938        let err = parse_byte_size("1.0MiB").unwrap_err();
2939        assert!(
2940            matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "1.0"),
2941            "got {err:?}"
2942        );
2943    }
2944
2945    #[test]
2946    fn parse_byte_size_rejects_half_gib() {
2947        // `"0.5GiB"` parses to 536870912 bytes = 512MiB; the renderer
2948        // emits `"512MiB"` on the next serialize. Pin the round-trip
2949        // drift on the explicitly-fractional case sized to land on a
2950        // unit boundary, so the gate's coverage includes both the
2951        // "doesn't land on a boundary" (1.5KiB → 1536) and "lands on
2952        // a smaller-unit boundary" (0.5GiB → 512MiB) drift shapes.
2953        let err = parse_byte_size("0.5GiB").unwrap_err();
2954        assert!(
2955            matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "0.5"),
2956            "got {err:?}"
2957        );
2958    }
2959
2960    #[test]
2961    fn parse_byte_size_rejects_scientific_notation_via_unit_arm() {
2962        // Scientific-notation magnitudes are canonical-form drift too
2963        // — the renderer never emits `"1e3KiB"` for any value. But
2964        // they're caught on a *different* arm than the fractional /
2965        // leading-`+` shapes: the parser's split-on-first-alphabetic-
2966        // byte heuristic reads the `e` as a unit prefix, so the input
2967        // falls into the existing `UnknownByteUnit { unit: "e3KiB" }`
2968        // diagnostic before the `NonIntegerByteMagnitude` gate is
2969        // consulted. Pin this dispatch path so a future relaxation of
2970        // the split heuristic (e.g. recognizing `e` as part of a
2971        // scientific-notation magnitude) surfaces here as a test
2972        // failure — at which point the `NonIntegerByteMagnitude` gate
2973        // would correctly take over, and this test would flip to that
2974        // arm with no other change required.
2975        let err = parse_byte_size("1e3KiB").unwrap_err();
2976        assert!(
2977            matches!(err, LimitsError::UnknownByteUnit { ref unit } if unit == "e3KiB"),
2978            "got {err:?}"
2979        );
2980    }
2981
2982    #[test]
2983    fn parse_byte_size_rejects_leading_plus() {
2984        // `"+1024"` parses through f64 as 1024 bytes; the renderer
2985        // emits `"1KiB"` on the next serialize. The leading `+` is
2986        // not a renderer-emitted shape, so it falls in the same
2987        // canonical-drift class as the fractional / scientific forms
2988        // — surfacing under the same diagnostic keeps the gate's
2989        // coverage uniform across every non-canonical-but-numeric
2990        // input shape the parser would otherwise accept.
2991        let err = parse_byte_size("+1024").unwrap_err();
2992        assert!(
2993            matches!(err, LimitsError::NonIntegerByteMagnitude { ref value } if value == "+1024"),
2994            "got {err:?}"
2995        );
2996    }
2997
2998    #[test]
2999    fn parse_byte_size_continues_to_accept_integer_magnitudes() {
3000        // The complement-side pin: every canonical integer-magnitude
3001        // form the renderer emits must continue to parse to the same
3002        // value the renderer produced. Sweep the five canonical
3003        // authoring shapes (unitless integer, KiB, MiB, GiB, KB) so a
3004        // future tightening of the parser surfaces here as a test
3005        // failure rather than a silent regression in the canonical
3006        // authoring set.
3007        assert_eq!(parse_byte_size("1024").unwrap(), 1024);
3008        assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
3009        assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3010        assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3011        assert_eq!(parse_byte_size("1000KB").unwrap(), 1_000_000);
3012    }
3013
3014    #[test]
3015    fn parse_byte_size_round_trips_through_render_for_every_canonical_form() {
3016        // The structural property the gate makes load-bearing: every
3017        // value the parser accepts round-trips through `render_byte_size`
3018        // to a string the parser also accepts — and to the *same* value.
3019        // Sweep the values the renderer emits canonically (1024 / 1MiB
3020        // / 1GiB / 1536 / 64MiB) so a future codec change that breaks
3021        // round-trip convergence surfaces here, not at a downstream
3022        // renderer that double-emits a typed slot.
3023        for n in [1u64, 1023, 1024, 1536, 64 * 1024 * 1024, 1024 * 1024 * 1024] {
3024            let rendered = render_byte_size(n);
3025            let reparsed = parse_byte_size(&rendered)
3026                .unwrap_or_else(|e| panic!("render({n}) = {rendered:?} must reparse, got {e:?}"));
3027            assert_eq!(
3028                reparsed, n,
3029                "round-trip drift on {n}: rendered={rendered:?}, reparsed={reparsed}",
3030            );
3031        }
3032    }
3033
3034    #[test]
3035    fn parse_byte_size_keeps_bad_magnitude_for_unparseable_input() {
3036        // The precedence pin: the new `NonIntegerByteMagnitude` arm
3037        // distinguishes *non-canonical-but-numeric* (`"1.5"`, `"1.0"`,
3038        // `"+1024"`, `"-1"`) from *genuinely-unparseable* (`"abc"`,
3039        // `"--1"`) so the existing `BadByteMagnitude` diagnostic's
3040        // wording remains load-bearing for the latter class — the gate
3041        // is additive, not replacing. Pin both arms so a future
3042        // relaxation that collapses them surfaces here.
3043        let err = parse_byte_size("abc").unwrap_err();
3044        assert!(
3045            matches!(err, LimitsError::BadByteMagnitude(_)),
3046            "got {err:?}"
3047        );
3048        let err = parse_byte_size("--1").unwrap_err();
3049        assert!(
3050            matches!(err, LimitsError::BadByteMagnitude(_)),
3051            "got {err:?}"
3052        );
3053    }
3054
3055    #[test]
3056    fn parse_byte_size_overflow_surfaces_as_bad_magnitude() {
3057        // `u64::MAX KiB` overflows the u64 result; the parser surfaces
3058        // the overflow as a `BadByteMagnitude` (not as a saturated
3059        // `u64::MAX` value that the wasm32-cap validate gate then
3060        // catches), so the diagnostic names the offending magnitude ×
3061        // unit pair at parse time rather than as
3062        // `MemoryExceedsWasm32Cap { bytes: u64::MAX }` far from the
3063        // author's intent. (`u64::MAX` itself parses cleanly with no
3064        // unit since `u64::MAX × 1 = u64::MAX` fits.)
3065        let err = parse_byte_size("18446744073709551615KiB").unwrap_err();
3066        let LimitsError::BadByteMagnitude(reason) = err else {
3067            panic!("expected BadByteMagnitude(overflow), got other variant");
3068        };
3069        assert!(
3070            reason.contains("overflow"),
3071            "overflow diagnostic must mention overflow (got {reason:?})"
3072        );
3073    }
3074
3075    // ── canonical-form: leading-zero byte-size codec gate ─────────────────
3076    //
3077    // Direct successor to the `parse_duration` leading-zero arm (39762d7),
3078    // the `supervisor::duration_codec` leading-zero arm (9178904), and the
3079    // `rate_limit_codec` leading-zero arm (4f46830) — the same canonical-
3080    // form render-determinism axis applied to the last typed-numeric codec
3081    // that still admitted leading-zero magnitudes. The digit-only gate
3082    // immediately above accepts every `u64::from_str`-parseable magnitude
3083    // including leading-zero padding, but `render_byte_size` always emits
3084    // the stripped form (`64MiB`, never `064MiB`) — silently drifting the
3085    // canonical string across a parse/render round-trip. Pins each
3086    // canonical leading-zero shape across the unit-set the codec admits
3087    // (KB / MB / GB / KiB / MiB / GiB / bare-integer), the all-zero
3088    // degenerate case, the codec-vs-validate-layer partition (single-byte
3089    // `"0"` stays accepted at the codec because the typed-validate gate
3090    // `MemoryZero` refuses semantic-zero authoring), the complement-side
3091    // pin (`1`..=`9`-led magnitudes stay accepted), and the serde-path pin
3092    // (the gate fires at deserialize, before any validate gate runs).
3093
3094    #[test]
3095    fn parse_byte_size_rejects_leading_zero_magnitude() {
3096        // The fail-before-pass-after pin: `"064MiB"` parsed cleanly on
3097        // every pre-gate codebase (`u64::from_str` accepts the leading
3098        // zero), the codec produced 64 MiB, and
3099        // `render_byte_size(64*1024*1024)` emitted `"64MiB"` on the next
3100        // serialize — silently dropping the leading zero and drifting
3101        // the canonical form away from the author's intent. The new
3102        // gate surfaces the round-trip break at the parser layer with a
3103        // self-locating diagnostic, peer with
3104        // `parse_duration_rejects_leading_zero_magnitude` on the sibling
3105        // codec.
3106        let err = parse_byte_size("064MiB").unwrap_err();
3107        assert!(
3108            matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "064"),
3109            "got {err:?}"
3110        );
3111    }
3112
3113    #[test]
3114    fn parse_byte_size_rejects_multi_digit_zero_magnitude() {
3115        // `"00MiB"` is the degenerate leading-zero case — every byte is
3116        // `0`. `u64::from_str("00")` = 0, and the codec produces 0;
3117        // `render_byte_size(0)` emits `"0"` on the next serialize —
3118        // drift from `"00MiB"` to `"0"`. The leading-zero arm refuses
3119        // the drift class at the codec layer while leaving the
3120        // canonical single-byte `"0"` accepted. Peer with
3121        // `parse_duration_rejects_multi_digit_zero_magnitude` on the
3122        // sibling codec.
3123        let err = parse_byte_size("00MiB").unwrap_err();
3124        assert!(
3125            matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "00"),
3126            "got {err:?}"
3127        );
3128    }
3129
3130    #[test]
3131    fn parse_byte_size_rejects_leading_zero_in_gib_unit() {
3132        // `"01GiB"` parses to 1 GiB; the renderer emits `"1GiB"` on the
3133        // next serialize. The leading-zero class is a property of the
3134        // magnitude, not the unit — pin a per-GiB magnitude alongside
3135        // the per-MiB / per-KiB / bare-integer pins so the gate's
3136        // coverage is structural across every canonical unit suffix
3137        // the codec accepts. Mirrors the per-hour pin
3138        // `parse_duration_rejects_leading_zero_in_hour_window` carries
3139        // on the sibling codec.
3140        let err = parse_byte_size("01GiB").unwrap_err();
3141        assert!(
3142            matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01"),
3143            "got {err:?}"
3144        );
3145    }
3146
3147    #[test]
3148    fn parse_byte_size_rejects_leading_zero_in_kib_unit() {
3149        // `"0512KiB"` parses to 512 KiB; the renderer emits `"512KiB"`
3150        // on the next serialize. Pin the per-KiB magnitude alongside
3151        // the per-MiB / per-GiB pins so the gate's coverage extends to
3152        // the smallest-unit power-of-1024 suffix the codec admits.
3153        let err = parse_byte_size("0512KiB").unwrap_err();
3154        assert!(
3155            matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "0512"),
3156            "got {err:?}"
3157        );
3158    }
3159
3160    #[test]
3161    fn parse_byte_size_rejects_leading_zero_in_decimal_units() {
3162        // `"0500MB"` parses to 500 MB (decimal-unit family — `KB` /
3163        // `MB` / `GB` powers of 1000, distinct from the `KiB` / `MiB` /
3164        // `GiB` powers-of-1024 family); the renderer emits the
3165        // appropriate canonical form on the next serialize. Pin the
3166        // decimal-unit family alongside the power-of-1024 family so the
3167        // gate's coverage is structural across both unit families the
3168        // codec admits.
3169        for (s, expected) in [("0500MB", "0500"), ("01KB", "01"), ("00GB", "00")] {
3170            let err = parse_byte_size(s).unwrap_err();
3171            assert!(
3172                matches!(err, LimitsError::LeadingZeroByteMagnitude { value: ref v } if v == expected),
3173                "got {err:?} for {s:?}"
3174            );
3175        }
3176    }
3177
3178    #[test]
3179    fn parse_byte_size_rejects_leading_zero_bare_integer() {
3180        // The bare-integer (no unit) shorthand inherits the leading-
3181        // zero arm: `"01024"` parses losslessly to 1024 bytes but
3182        // `render_byte_size(1024)` emits `"1KiB"` on the next serialize.
3183        // Pin the bare-integer path so a future relaxation that
3184        // special-cases the unitless shorthand surfaces here as a test
3185        // failure. Mirrors the bare-integer pin
3186        // `parse_duration_rejects_leading_zero_bare_integer_as_seconds`
3187        // carries on the sibling codec.
3188        let err = parse_byte_size("01024").unwrap_err();
3189        assert!(
3190            matches!(err, LimitsError::LeadingZeroByteMagnitude { ref value } if value == "01024"),
3191            "got {err:?}"
3192        );
3193    }
3194
3195    #[test]
3196    fn parse_byte_size_accepts_single_zero_magnitude_at_codec_layer() {
3197        // The codec-layer / typed-validate-layer boundary pin: the
3198        // single-byte `"0"` magnitude round-trips losslessly through
3199        // `render_byte_size` (`render_byte_size(0)` emits `"0"`), so it
3200        // stays accepted at this codec layer across every canonical
3201        // unit suffix. The downstream `LimitsError::MemoryZero` gate is
3202        // what refuses zero-magnitude authoring at the typed-validate
3203        // layer above — the partition keeps the canonical-form-drift
3204        // diagnostic (this arm) and the semantic-zero diagnostic (the
3205        // validate gate) disjoint. Mirrors the
3206        // `parse_duration_accepts_single_zero_magnitude_at_codec_layer`
3207        // partition pin on the sibling codec.
3208        assert_eq!(parse_byte_size("0").unwrap(), 0);
3209        assert_eq!(parse_byte_size("0B").unwrap(), 0);
3210        assert_eq!(parse_byte_size("0KiB").unwrap(), 0);
3211        assert_eq!(parse_byte_size("0MiB").unwrap(), 0);
3212        assert_eq!(parse_byte_size("0GiB").unwrap(), 0);
3213        assert_eq!(parse_byte_size("0KB").unwrap(), 0);
3214    }
3215
3216    #[test]
3217    fn parse_byte_size_accepts_canonical_magnitude_with_leading_one() {
3218        // The complement-side pin on the leading-zero arm: magnitudes
3219        // beginning with `1`..=`9` stay accepted across every canonical
3220        // unit suffix the codec accepts. Pin this so a future
3221        // tightening cannot drift into rejecting valid canonical
3222        // magnitudes — peer with the
3223        // `parse_duration_accepts_canonical_magnitude_with_leading_one`
3224        // pin on the sibling codec.
3225        assert_eq!(parse_byte_size("1").unwrap(), 1);
3226        assert_eq!(parse_byte_size("1KiB").unwrap(), 1024);
3227        assert_eq!(parse_byte_size("1MiB").unwrap(), 1024 * 1024);
3228        assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3229        assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3230        assert_eq!(parse_byte_size("9").unwrap(), 9);
3231    }
3232
3233    #[test]
3234    fn de_byte_size_rejects_leading_zero_through_serde() {
3235        // The serde-path pin: a `:limits :memory` carrying a
3236        // leading-zero magnitude (`"064MiB"`) must fail at deserialize
3237        // time, not silently round-trip the value through the parser.
3238        // The gate fires at deserialize, before any validate gate runs
3239        // — peer with `de_duration_rejects_leading_zero_through_serde`
3240        // on the sibling codec.
3241        let json = r#"{"memory":"064MiB"}"#;
3242        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
3243        let msg = err.to_string();
3244        assert!(
3245            msg.contains("leading zero"),
3246            "serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
3247        );
3248    }
3249
3250    // ── canonical-form: whitespace-rejection byte-size codec gate ─────────
3251    //
3252    // Direct successor to the `parse_duration` whitespace-rejection arm
3253    // (ebc3a75), the `supervisor::duration_codec` whitespace-rejection
3254    // arm (a7ae622), and the `rate_limit_codec` whitespace-rejection arm
3255    // (1ad7755) on the same canonical-form render-determinism axis. The
3256    // pre-gate top-level `s.trim()` at parse entry and the per-part
3257    // `num_part.trim()` / `unit.trim()` calls silently ate leading /
3258    // trailing / internal whitespace, so every whitespace-carrying
3259    // shape parsed to the same byte magnitude and round-tripped through
3260    // `render_byte_size` to a *different* canonical string on next
3261    // serialize — the same canonical-form-drift class the leading-`+` /
3262    // fractional / leading-zero arms already close on this codec.
3263    // `u8::is_ascii_whitespace` covers the five WhatWG-conformant ASCII
3264    // whitespace bytes (space `0x20`, tab `0x09`, LF `0x0A`, FF `0x0C`,
3265    // CR `0x0D`). Closes the whitespace-rejection axis across every
3266    // typed-magnitude codec in caixa-core.
3267
3268    #[test]
3269    fn parse_byte_size_rejects_leading_whitespace() {
3270        // The fail-before-pass-after pin: `" 64MiB"` — the canonical
3271        // paste-from-aligned-doc / paste-from-YAML-quoted-plain-scalar
3272        // footgun. Before this gate the top-level `s.trim()` at parse
3273        // entry silently ate the leading space and parsed the value to
3274        // 64 * 1024 * 1024 bytes, which then round-tripped through
3275        // `render_byte_size` to `"64MiB"` (a *different* canonical
3276        // string on the next emit) — the exact canonical-form-drift
3277        // class the leading-`+` / leading-zero arms already close,
3278        // extended to the whitespace-byte class. Peer with the sibling
3279        // `parse_duration_rejects_leading_whitespace` arm (ebc3a75) on
3280        // the shared canonical-form-drift trajectory.
3281        let err = parse_byte_size(" 64MiB").unwrap_err();
3282        assert!(
3283            matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == " 64MiB" && byte == 0x20),
3284            "got {err:?}"
3285        );
3286        let msg = err.to_string();
3287        assert!(
3288            msg.contains("whitespace byte 0x20"),
3289            "diagnostic must surface the offending byte verbatim (got {msg:?})"
3290        );
3291        assert!(
3292            msg.contains("THEORY.md"),
3293            "diagnostic must cite the render-determinism contract (got {msg:?})"
3294        );
3295    }
3296
3297    #[test]
3298    fn parse_byte_size_rejects_trailing_whitespace() {
3299        // `"64MiB "` — the canonical shell-history / trailing-space
3300        // paste footgun. Before this gate the top-level `s.trim()`
3301        // silently ate the trailing space and parsed to 64 * 1024 *
3302        // 1024 bytes, round-tripping to `"64MiB"` on the next emit —
3303        // same canonical-form drift as the leading-space sibling,
3304        // closed on the same whitespace-byte arm.
3305        let err = parse_byte_size("64MiB ").unwrap_err();
3306        assert!(
3307            matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB " && byte == 0x20),
3308            "got {err:?}"
3309        );
3310    }
3311
3312    #[test]
3313    fn parse_byte_size_rejects_internal_whitespace_between_magnitude_and_unit() {
3314        // `"64 MiB"` — the canonical typographically-spaced author
3315        // shape (the same idiom every prose reference to a byte-size
3316        // renders as, mistakenly retained when the value is pasted
3317        // into a codec-shaped slot). Before this gate the per-part
3318        // `num_part.trim()` / `unit.trim()` calls silently ate the
3319        // whitespace between the magnitude and the unit and parsed the
3320        // value to 64 * 1024 * 1024 bytes, round-tripping to `"64MiB"`
3321        // — the codec's *internal* whitespace-tolerance vector,
3322        // orthogonal to the leading / trailing surface but the same
3323        // canonical-form-drift class. Pins the arm as strictly
3324        // stronger than the pre-existing top-level `s.trim()`
3325        // behavior: it fires on whitespace anywhere in the value, not
3326        // just at the string boundary.
3327        let err = parse_byte_size("64 MiB").unwrap_err();
3328        assert!(
3329            matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64 MiB" && byte == 0x20),
3330            "got {err:?}"
3331        );
3332    }
3333
3334    #[test]
3335    fn parse_byte_size_rejects_tab_byte() {
3336        // `"\t64MiB"` — the canonical paste-from-indented-doc /
3337        // paste-from-YAML-block-scalar footgun where a tab byte leads
3338        // the magnitude. Pins that the gate covers tab (`0x09`) as
3339        // well as space (`0x20`) — both are `u8::is_ascii_whitespace`
3340        // members and both would be silently swallowed by `s.trim()`
3341        // pre-gate. The `is_ascii_whitespace` coverage extends beyond
3342        // space alone to the full ASCII-whitespace set (space `0x20`,
3343        // tab `0x09`, LF `0x0A`, FF `0x0C`, CR `0x0D`); this test pins
3344        // the tab arm as a representative of the non-space members.
3345        let err = parse_byte_size("\t64MiB").unwrap_err();
3346        assert!(
3347            matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "\t64MiB" && byte == 0x09),
3348            "got {err:?}"
3349        );
3350    }
3351
3352    #[test]
3353    fn parse_byte_size_rejects_trailing_newline() {
3354        // `"64MiB\n"` — the canonical multi-line-paste footgun where
3355        // a trailing LF byte survives the paste. Pins the LF member
3356        // (`0x0A`) of the `is_ascii_whitespace` set as a peer to the
3357        // space and tab pins above — every non-space non-tab
3358        // whitespace byte the WhatWG ASCII-whitespace set covers is
3359        // refused by the same arm.
3360        let err = parse_byte_size("64MiB\n").unwrap_err();
3361        assert!(
3362            matches!(err, LimitsError::WhitespaceInByteSize { ref value, byte } if value == "64MiB\n" && byte == 0x0a),
3363            "got {err:?}"
3364        );
3365    }
3366
3367    #[test]
3368    fn parse_byte_size_accepts_whitespace_free_canonical_forms() {
3369        // The complement-side pin: every canonical whitespace-free
3370        // authoring form the renderer emits stays accepted post-gate.
3371        // Sweep the canonical unit suffixes plus the bare-integer
3372        // shorthand so a future tightening of the whitespace arm that
3373        // over-fires on the accepted set surfaces here as a test
3374        // failure. Peer with the
3375        // `parse_duration_accepts_whitespace_free_canonical_forms` pin
3376        // on the sibling codec.
3377        assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3378        assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3379        assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
3380        assert_eq!(parse_byte_size("1KB").unwrap(), 1_000);
3381        assert_eq!(parse_byte_size("1024").unwrap(), 1024);
3382        assert_eq!(parse_byte_size("0").unwrap(), 0);
3383    }
3384
3385    #[test]
3386    fn de_byte_size_rejects_whitespace_through_serde() {
3387        // The serde-path pin: a `:limits :memory` carrying a
3388        // whitespace-byte-carrying value (`" 64MiB"`) must fail at
3389        // deserialize time, not silently round-trip the value through
3390        // the pre-existing top-level `s.trim()`. The gate fires at
3391        // deserialize, before any validate gate runs — peer with the
3392        // existing `de_byte_size_rejects_leading_zero_through_serde` /
3393        // `de_duration_rejects_whitespace_through_serde` pins on the
3394        // same canonical-form-drift axis.
3395        let json = r#"{"memory":" 64MiB"}"#;
3396        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
3397        let msg = err.to_string();
3398        assert!(
3399            msg.contains("whitespace byte"),
3400            "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
3401        );
3402        assert!(
3403            msg.contains("0x20"),
3404            "serde diagnostic must name the offending byte (got {msg:?})"
3405        );
3406
3407        // The whitespace-free complement — same author-side intent,
3408        // written in the canonical form the renderer would emit,
3409        // deserializes cleanly.
3410        let json = r#"{"memory":"64MiB"}"#;
3411        let l: LimitsSpec = serde_json::from_str(json).unwrap();
3412        assert_eq!(l.memory, Some(64 * 1024 * 1024));
3413    }
3414
3415    // ── canonical-form: non-ASCII Unicode `White_Space` byte-size gate ────
3416    //
3417    // Direct successor to the `parse_byte_size` ASCII-whitespace arm
3418    // (24a8ad4) — closes the strictly-complementary class the byte-scan
3419    // above cannot see. `str::trim` uses `char::is_whitespace` (Unicode
3420    // `White_Space`, strictly wider than the ASCII byte set); a leading /
3421    // trailing / internal NBSP (`\u{00A0}`) / LINE SEPARATOR (`\u{2028}`)
3422    // / EM-SPACE (`\u{2003}`) survives the byte-scan but is silently
3423    // stripped by the top-level trim, drifting to canonical `"64MiB"` on
3424    // round-trip. Pins the arm through the lifted
3425    // [`crate::render::find_non_ascii_whitespace_char`] predicate.
3426
3427    #[test]
3428    fn parse_byte_size_rejects_leading_nbsp() {
3429        // NBSP (`\u{00A0}` = UTF-8 `0xC2 0xA0`) — the canonical
3430        // paste-from-typography / paste-from-word-processor footgun.
3431        // Before this arm landed the byte-scan missed it (neither `0xC2`
3432        // nor `0xA0` is `is_ascii_whitespace`) and `str::trim` at parse
3433        // entry silently stripped it, yielding the same `64 * 1024 *
3434        // 1024` bytes as the whitespace-free canonical form and drifting
3435        // to `"64MiB"` on next serialize.
3436        let s = "\u{00A0}64MiB";
3437        let err = parse_byte_size(s).unwrap_err();
3438        assert!(
3439            matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
3440            "got {err:?}"
3441        );
3442        let msg = err.to_string();
3443        assert!(
3444            msg.contains("U+00A0"),
3445            "diagnostic must surface the codepoint verbatim (got {msg:?})"
3446        );
3447        assert!(
3448            msg.contains("THEORY.md"),
3449            "diagnostic must cite the render-determinism contract (got {msg:?})"
3450        );
3451    }
3452
3453    #[test]
3454    fn parse_byte_size_rejects_internal_line_separator() {
3455        // LINE SEPARATOR (`\u{2028}`) between magnitude and unit — the
3456        // canonical paste-from-web-doc footgun (many rendering engines
3457        // insert `\u{2028}` at soft-wrap boundaries in RTF/HTML → plain
3458        // text conversion). Pins the arm on a non-space non-NBSP Unicode
3459        // `White_Space` member.
3460        let s = "64\u{2028}MiB";
3461        let err = parse_byte_size(s).unwrap_err();
3462        assert!(
3463            matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
3464            "got {err:?}"
3465        );
3466    }
3467
3468    #[test]
3469    fn parse_byte_size_rejects_trailing_ideographic_space() {
3470        // IDEOGRAPHIC SPACE (`\u{3000}`) — the CJK-typography paste
3471        // footgun (canonical U+3000 is the full-width space that
3472        // Japanese / Chinese IMEs emit when input is auto-widened). Pins
3473        // the arm at the top edge of the `char::is_whitespace` set.
3474        let s = "64MiB\u{3000}";
3475        let err = parse_byte_size(s).unwrap_err();
3476        assert!(
3477            matches!(err, LimitsError::NonAsciiWhitespaceInByteSize { ref value, ch, codepoint } if value == s && ch == '\u{3000}' && codepoint == 0x3000),
3478            "got {err:?}"
3479        );
3480    }
3481
3482    #[test]
3483    fn parse_byte_size_accepts_ascii_only_canonical_forms_after_unicode_arm() {
3484        // Positive-control pin: every ASCII-only canonical form the
3485        // renderer emits stays accepted through the new arm — the
3486        // lifted predicate is a strict no-op on ASCII input.
3487        assert_eq!(parse_byte_size("64MiB").unwrap(), 64 * 1024 * 1024);
3488        assert_eq!(parse_byte_size("1GiB").unwrap(), 1024 * 1024 * 1024);
3489        assert_eq!(parse_byte_size("512KiB").unwrap(), 512 * 1024);
3490        assert_eq!(parse_byte_size("1024").unwrap(), 1024);
3491    }
3492
3493    // ── canonical-form: integer-magnitude duration codec gate ─────────────
3494    //
3495    // Direct successor to the `parse_byte_size` integer-magnitude gate on
3496    // the peer `:limits :memory` codec — every magnitude `render_duration`
3497    // emits is a non-negative integer (no decimal point, no leading sign,
3498    // no scientific notation). The parser's accepted set must match for
3499    // parse → render → parse to round-trip without canonical-form drift.
3500    // Pins every canonical-drift shape — fractional (`"1.5s"`),
3501    // decimal-shaped-integer (`"1.0s"`), half-unit (`"0.5m"`),
3502    // leading-`+` (`"+30s"`), leading-`-` (`"-30s"`) — plus the
3503    // complement-side pin (integer happy paths), the round-trip
3504    // convergence property, the BadDurationMagnitude-precedence pin
3505    // (genuinely unparseable inputs keep their narrower diagnostic), the
3506    // overflow-surface pin (u64-overflow on magnitude × unit surfaces at
3507    // parse time), and the serde-path pin (the gate fires at deserialize,
3508    // before any validate gate runs).
3509
3510    #[test]
3511    fn parse_duration_rejects_fractional_seconds() {
3512        // The fail-before-pass-after pin: `"1.5s"` parsed cleanly on
3513        // every pre-gate codebase (f64::parse accepts the decimal), the
3514        // codec produced 1500ms, and `render_duration(1500ms)` emitted
3515        // `"1500ms"` on the next serialize — silently drifting the
3516        // canonical form away from the author's intent. The new gate
3517        // surfaces the round-trip break at the parser layer with a
3518        // self-locating diagnostic.
3519        let err = parse_duration("1.5s").unwrap_err();
3520        assert!(
3521            matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.5"),
3522            "got {err:?}"
3523        );
3524    }
3525
3526    #[test]
3527    fn parse_duration_rejects_decimal_shaped_integer() {
3528        // The canonical-drift case where the *value* is integer but the
3529        // *form* carries a redundant decimal point — `"1.0s"` parses to
3530        // 1s (integer), but the renderer emits `"1s"` on the next
3531        // serialize (no decimal point). The parse-shape gate fires here
3532        // too so the codec's accepted set is exactly the renderer's
3533        // emitted set.
3534        let err = parse_duration("1.0s").unwrap_err();
3535        assert!(
3536            matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "1.0"),
3537            "got {err:?}"
3538        );
3539    }
3540
3541    #[test]
3542    fn parse_duration_rejects_half_minute() {
3543        // `"0.5m"` parses to 30s; the renderer emits `"30s"` on the
3544        // next serialize. Pin the round-trip drift on the explicitly-
3545        // fractional case sized to land on a smaller-unit boundary, so
3546        // the gate's coverage includes both the "doesn't land on a
3547        // boundary" (1.5s → 1500ms) and "lands on a smaller-unit
3548        // boundary" (0.5m → 30s) drift shapes — the same two-shape
3549        // pattern the byte-size gate covers (1.5KiB → 1536, 0.5GiB →
3550        // 512MiB).
3551        let err = parse_duration("0.5m").unwrap_err();
3552        assert!(
3553            matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "0.5"),
3554            "got {err:?}"
3555        );
3556    }
3557
3558    #[test]
3559    fn parse_duration_rejects_leading_plus() {
3560        // `"+30s"` parses through f64 as 30s; the renderer emits `"30s"`
3561        // on the next serialize. The leading `+` is not a renderer-
3562        // emitted shape, so it falls in the same canonical-drift class
3563        // as the fractional forms — surfacing under the same diagnostic
3564        // keeps the gate's coverage uniform across every non-canonical-
3565        // but-numeric input shape the parser would otherwise accept.
3566        let err = parse_duration("+30s").unwrap_err();
3567        assert!(
3568            matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "+30"),
3569            "got {err:?}"
3570        );
3571    }
3572
3573    #[test]
3574    fn parse_duration_rejects_negative_seconds_via_integer_gate() {
3575        // The negative-magnitude class — pre-gate the parser routed
3576        // negatives through the `num < 0.0` check to `BadDurationMagnitude`;
3577        // the new digit-only gate fires earlier and routes the same
3578        // input to `NonIntegerDurationMagnitude` (negatives are not
3579        // digit-only). Pin the new diagnostic so a future relaxation
3580        // that re-routes negatives back to the old arm surfaces here.
3581        let err = parse_duration("-30s").unwrap_err();
3582        assert!(
3583            matches!(err, LimitsError::NonIntegerDurationMagnitude { ref value } if value == "-30"),
3584            "got {err:?}"
3585        );
3586    }
3587
3588    #[test]
3589    fn parse_duration_continues_to_accept_integer_magnitudes() {
3590        // The complement-side pin: every canonical integer-magnitude
3591        // form the renderer emits must continue to parse to the same
3592        // value the renderer produced. Sweep the canonical authoring
3593        // shapes (ms, bare-s, s, m, h, and the bare-integer "0" zero-
3594        // shape) so a future tightening of the parser surfaces here as
3595        // a test failure rather than a silent regression.
3596        assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
3597        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
3598        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
3599        assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
3600        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
3601        assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
3602    }
3603
3604    #[test]
3605    fn parse_duration_round_trips_through_render_for_every_canonical_form() {
3606        // The structural property the gate makes load-bearing: every
3607        // value the parser accepts round-trips through the canonical
3608        // [`crate::supervisor::duration_codec::render`] primitive to a
3609        // string the parser also accepts — and to the *same* value.
3610        // Sweep the values the renderer emits canonically (ms / s / m /
3611        // h boundaries plus a non-aligned millisecond) so a future
3612        // codec change that breaks round-trip convergence surfaces here.
3613        for d in [
3614            Duration::from_millis(1),
3615            Duration::from_millis(500),
3616            Duration::from_millis(1500),
3617            Duration::from_secs(1),
3618            Duration::from_secs(30),
3619            Duration::from_secs(60),
3620            Duration::from_secs(120),
3621            Duration::from_secs(3600),
3622        ] {
3623            let rendered = crate::supervisor::duration_codec::render(d);
3624            let reparsed = parse_duration(&rendered)
3625                .unwrap_or_else(|e| panic!("render({d:?}) = {rendered:?} must reparse, got {e:?}"));
3626            assert_eq!(
3627                reparsed, d,
3628                "round-trip drift on {d:?}: rendered={rendered:?}, reparsed={reparsed:?}",
3629            );
3630        }
3631    }
3632
3633    #[test]
3634    fn parse_duration_keeps_bad_magnitude_for_unparseable_input() {
3635        // The precedence pin: the new `NonIntegerDurationMagnitude` arm
3636        // distinguishes *non-canonical-but-numeric* (`"1.5"`, `"+30"`,
3637        // `"-30"`) from *genuinely-unparseable* (`"abc"`, `"--1"`) so
3638        // the existing `BadDurationMagnitude` diagnostic's wording
3639        // remains load-bearing for the latter class — the gate is
3640        // additive, not replacing.
3641        let err = parse_duration("abcs").unwrap_err();
3642        assert!(
3643            matches!(err, LimitsError::BadDurationMagnitude(_)),
3644            "got {err:?}"
3645        );
3646        let err = parse_duration("--1s").unwrap_err();
3647        assert!(
3648            matches!(err, LimitsError::BadDurationMagnitude(_)),
3649            "got {err:?}"
3650        );
3651    }
3652
3653    #[test]
3654    fn parse_duration_overflow_surfaces_as_bad_magnitude() {
3655        // `u64::MAX h` overflows the seconds computation (magnitude ×
3656        // 3600); the parser surfaces the overflow as a
3657        // `BadDurationMagnitude` with an overflow-shaped wording so the
3658        // diagnostic names the offending magnitude × unit pair at parse
3659        // time. Matches `parse_byte_size`'s overflow-surface arm
3660        // structurally.
3661        let err = parse_duration("18446744073709551615h").unwrap_err();
3662        let LimitsError::BadDurationMagnitude(reason) = err else {
3663            panic!("expected BadDurationMagnitude(overflow), got other variant");
3664        };
3665        assert!(
3666            reason.contains("overflow"),
3667            "overflow diagnostic must mention overflow (got {reason:?})"
3668        );
3669    }
3670
3671    // ── canonical-form: leading-zero duration codec gate ─────────────────
3672    //
3673    // Direct successor to the `supervisor::duration_codec` leading-zero
3674    // arm (9178904) and the `rate_limit_codec` leading-zero arm (4f46830)
3675    // — closes the leading-zero canonical-form-drift class on the
3676    // `:limits :wall-clock` codec. Every magnitude `render_duration`
3677    // emits is a non-negative integer with no leading-zero padding; the
3678    // parser's accepted set must match for parse → render → parse to
3679    // round-trip without canonical-form drift. The single-byte `"0"`
3680    // round-trips losslessly (`render_duration(Duration::ZERO)` emits
3681    // `"0s"`) and the downstream [`LimitsError::WallClockZero`] gate
3682    // refuses zero-magnitude authoring at the typed-validate layer above
3683    // — the codec-layer / typed-validate-layer partition is what keeps
3684    // the diagnostic partitioning stable.
3685
3686    #[test]
3687    fn parse_duration_rejects_leading_zero_magnitude() {
3688        // The fail-before-pass-after pin: `"030s"` parsed cleanly on
3689        // every pre-gate codebase (`u64::from_str` accepts the leading
3690        // zero), the codec produced 30s, and `render_duration(30s)`
3691        // emitted `"30s"` on the next serialize — silently dropping
3692        // the leading zero and drifting the canonical form away from
3693        // the author's intent. The new gate surfaces the round-trip
3694        // break at the parser layer with a self-locating diagnostic.
3695        let err = parse_duration("030s").unwrap_err();
3696        assert!(
3697            matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
3698            "got {err:?}"
3699        );
3700    }
3701
3702    #[test]
3703    fn parse_duration_rejects_multi_digit_zero_magnitude() {
3704        // `"00s"` is the degenerate leading-zero case — every byte is
3705        // `0`. `u64::from_str("00")` = 0, and the codec produces
3706        // `Duration::ZERO`; `render_duration(Duration::ZERO)` emits
3707        // `"0s"` on the next serialize — drift from `"00s"` to `"0s"`.
3708        // The leading-zero arm refuses the drift class at the codec
3709        // layer while leaving the canonical single-byte `"0s"` accepted.
3710        let err = parse_duration("00s").unwrap_err();
3711        assert!(
3712            matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "00"),
3713            "got {err:?}"
3714        );
3715    }
3716
3717    #[test]
3718    fn parse_duration_rejects_leading_zero_in_hour_window() {
3719        // `"01h"` parses to 1h; the renderer emits `"1h"` on the next
3720        // serialize. The leading-zero class is a property of the
3721        // magnitude, not the unit — pin a per-hour magnitude alongside
3722        // the per-second / per-ms pins so the gate's coverage is
3723        // structural across every canonical unit suffix the codec
3724        // accepts. Mirrors the `_per_hour_window` pin the
3725        // `supervisor::duration_codec` and `rate_limit_codec` leading-
3726        // zero arms carry on the peer codecs.
3727        let err = parse_duration("01h").unwrap_err();
3728        assert!(
3729            matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "01"),
3730            "got {err:?}"
3731        );
3732    }
3733
3734    #[test]
3735    fn parse_duration_rejects_leading_zero_bare_integer_as_seconds() {
3736        // The bare-integer-as-seconds shorthand (`"30"` → 30s, no unit
3737        // suffix because the parser routes the empty `unit` slot to
3738        // `Duration::from_secs`) inherits the leading-zero arm: `"030"`
3739        // parses losslessly to 30s but `render_duration(30s)` emits
3740        // `"30s"` on the next serialize. Pin the bare-integer path so a
3741        // future relaxation that special-cases the unitless shorthand
3742        // surfaces here as a test failure.
3743        let err = parse_duration("030").unwrap_err();
3744        assert!(
3745            matches!(err, LimitsError::LeadingZeroDurationMagnitude { ref value } if value == "030"),
3746            "got {err:?}"
3747        );
3748    }
3749
3750    #[test]
3751    fn parse_duration_accepts_single_zero_magnitude_at_codec_layer() {
3752        // The codec-layer / typed-validate-layer boundary pin: the
3753        // single-byte `"0"` magnitude round-trips losslessly through
3754        // `render_duration` (`render_duration(Duration::ZERO)` emits
3755        // `"0s"`), so it stays accepted at this codec layer across
3756        // every canonical unit suffix. The downstream
3757        // `LimitsError::WallClockZero` gate is what refuses
3758        // zero-magnitude authoring at the typed-validate layer above
3759        // — the partition keeps the canonical-form-drift diagnostic
3760        // (this arm) and the semantic-zero diagnostic (the validate
3761        // gate) disjoint.
3762        assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
3763        assert_eq!(parse_duration("0ms").unwrap(), Duration::ZERO);
3764        assert_eq!(parse_duration("0m").unwrap(), Duration::ZERO);
3765        assert_eq!(parse_duration("0h").unwrap(), Duration::ZERO);
3766        assert_eq!(parse_duration("0").unwrap(), Duration::ZERO);
3767    }
3768
3769    #[test]
3770    fn parse_duration_accepts_canonical_magnitude_with_leading_one() {
3771        // The complement-side pin on the leading-zero arm: magnitudes
3772        // beginning with `1`..=`9` stay accepted across every canonical
3773        // unit suffix the codec accepts. Pin this so a future
3774        // tightening cannot drift into rejecting valid canonical
3775        // magnitudes — peer with the `_accepts_canonical_magnitude_with_leading_one`
3776        // pin the `supervisor::duration_codec` and `rate_limit_codec`
3777        // leading-zero arms carry.
3778        assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
3779        assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
3780        assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
3781        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
3782        assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
3783        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
3784    }
3785
3786    // ── canonical-form: whitespace-rejection duration codec gate ─────────
3787    //
3788    // Direct successor to the `supervisor::duration_codec` whitespace-
3789    // rejection arm (a7ae622) and the `rate_limit_codec` whitespace-
3790    // rejection arm (1ad7755) on the same canonical-form
3791    // render-determinism axis. The pre-gate top-level `s.trim()` at
3792    // parse entry and the per-part `num_part.trim()` / `unit.trim()`
3793    // calls silently ate leading / trailing / internal whitespace, so
3794    // every whitespace-carrying shape parsed to the same integer
3795    // magnitude and round-tripped through `render_duration` to a
3796    // *different* canonical string on next serialize — the same
3797    // canonical-form-drift class the leading-`+` / fractional /
3798    // leading-zero arms already close on this codec. `u8::is_ascii_whitespace`
3799    // covers the five WhatWG-conformant ASCII whitespace bytes
3800    // (space `0x20`, tab `0x09`, LF `0x0A`, FF `0x0C`, CR `0x0D`).
3801
3802    #[test]
3803    fn parse_duration_rejects_leading_whitespace() {
3804        // The fail-before-pass-after pin: `" 30s"` — the canonical
3805        // paste-from-aligned-doc / paste-from-YAML-quoted-plain-scalar
3806        // footgun. Before this gate the top-level `s.trim()` at parse
3807        // entry silently ate the leading space and parsed the value to
3808        // `Duration::from_secs(30)`, which then round-tripped through
3809        // `render_duration` to `"30s"` (a *different* canonical string
3810        // on the next emit) — the exact canonical-form-drift class the
3811        // leading-`+` / leading-zero arms already close, extended to
3812        // the whitespace-byte class. Peer with the sibling
3813        // `supervisor::duration_codec` `parse_rejects_leading_whitespace`
3814        // arm (a7ae622) on the shared duration-codec trajectory.
3815        let err = parse_duration(" 30s").unwrap_err();
3816        assert!(
3817            matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == " 30s" && byte == 0x20),
3818            "got {err:?}"
3819        );
3820        let msg = err.to_string();
3821        assert!(
3822            msg.contains("whitespace byte 0x20"),
3823            "diagnostic must surface the offending byte verbatim (got {msg:?})"
3824        );
3825        assert!(
3826            msg.contains("THEORY.md"),
3827            "diagnostic must cite the render-determinism contract (got {msg:?})"
3828        );
3829    }
3830
3831    #[test]
3832    fn parse_duration_rejects_trailing_whitespace() {
3833        // `"30s "` — the canonical shell-history / trailing-space paste
3834        // footgun. Before this gate the top-level `s.trim()` silently
3835        // ate the trailing space and parsed to `Duration::from_secs(30)`,
3836        // round-tripping to `"30s"` on the next emit — same canonical-
3837        // form drift as the leading-space sibling, closed on the same
3838        // whitespace-byte arm.
3839        let err = parse_duration("30s ").unwrap_err();
3840        assert!(
3841            matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s " && byte == 0x20),
3842            "got {err:?}"
3843        );
3844    }
3845
3846    #[test]
3847    fn parse_duration_rejects_internal_whitespace_between_magnitude_and_unit() {
3848        // `"30 s"` — the canonical typographically-spaced author shape
3849        // (the same idiom every prose reference to a duration renders as,
3850        // mistakenly retained when the value is pasted into a codec-
3851        // shaped slot). Before this gate the per-part `num_part.trim()`
3852        // / `unit.trim()` calls silently ate the whitespace between the
3853        // magnitude and the unit and parsed the value to
3854        // `Duration::from_secs(30)`, round-tripping to `"30s"` — the
3855        // codec's *internal* whitespace-tolerance vector, orthogonal
3856        // to the leading / trailing surface but the same canonical-
3857        // form-drift class. Pins the arm as strictly stronger than the
3858        // pre-existing top-level `s.trim()` behavior: it fires on
3859        // whitespace anywhere in the value, not just at the string
3860        // boundary.
3861        let err = parse_duration("30 s").unwrap_err();
3862        assert!(
3863            matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30 s" && byte == 0x20),
3864            "got {err:?}"
3865        );
3866    }
3867
3868    #[test]
3869    fn parse_duration_rejects_tab_byte() {
3870        // `"\t30s"` — the canonical paste-from-indented-doc /
3871        // paste-from-YAML-block-scalar footgun where a tab byte leads
3872        // the magnitude. Pins that the gate covers tab (`0x09`) as well
3873        // as space (`0x20`) — both are `u8::is_ascii_whitespace` members
3874        // and both would be silently swallowed by `s.trim()` pre-gate.
3875        // The `is_ascii_whitespace` coverage extends beyond space alone
3876        // to the full ASCII-whitespace set (space `0x20`, tab `0x09`,
3877        // LF `0x0A`, FF `0x0C`, CR `0x0D`); this test pins the tab arm
3878        // as a representative of the non-space members.
3879        let err = parse_duration("\t30s").unwrap_err();
3880        assert!(
3881            matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "\t30s" && byte == 0x09),
3882            "got {err:?}"
3883        );
3884    }
3885
3886    #[test]
3887    fn parse_duration_rejects_trailing_newline() {
3888        // `"30s\n"` — the canonical multi-line-paste footgun where a
3889        // trailing LF byte survives the paste. Pins the LF member
3890        // (`0x0A`) of the `is_ascii_whitespace` set as a peer to the
3891        // space and tab pins above — every non-space non-tab whitespace
3892        // byte the WhatWG ASCII-whitespace set covers is refused by
3893        // the same arm.
3894        let err = parse_duration("30s\n").unwrap_err();
3895        assert!(
3896            matches!(err, LimitsError::WhitespaceInDuration { ref value, byte } if value == "30s\n" && byte == 0x0a),
3897            "got {err:?}"
3898        );
3899    }
3900
3901    #[test]
3902    fn parse_duration_accepts_whitespace_free_canonical_forms() {
3903        // The complement-side pin: every canonical whitespace-free
3904        // authoring form the renderer emits stays accepted post-gate.
3905        // Sweep the canonical unit suffixes plus the bare-integer
3906        // shorthand so a future tightening of the whitespace arm that
3907        // over-fires on the accepted set surfaces here as a test
3908        // failure. Peer with the `parse_duration_continues_to_accept_integer_magnitudes`
3909        // pin the fractional / leading-`+` gate carries.
3910        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
3911        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
3912        assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120));
3913        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
3914        assert_eq!(parse_duration("0s").unwrap(), Duration::ZERO);
3915        assert_eq!(parse_duration("3600").unwrap(), Duration::from_secs(3600));
3916    }
3917
3918    #[test]
3919    fn de_duration_rejects_whitespace_through_serde() {
3920        // The serde-path pin: a `:limits :wall-clock` carrying a
3921        // whitespace-byte-carrying value (`" 30s"`) must fail at
3922        // deserialize time, not silently round-trip the value through
3923        // the pre-existing top-level `s.trim()`. The gate fires at
3924        // deserialize, before any validate gate runs — peer with the
3925        // existing `de_duration_rejects_leading_zero_through_serde` /
3926        // `de_duration_rejects_fractional_value_through_serde` pins on
3927        // the same canonical-form-drift axis.
3928        let json = r#"{"wallClock":" 30s"}"#;
3929        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
3930        let msg = err.to_string();
3931        assert!(
3932            msg.contains("whitespace byte"),
3933            "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
3934        );
3935        assert!(
3936            msg.contains("0x20"),
3937            "serde diagnostic must name the offending byte (got {msg:?})"
3938        );
3939
3940        // The whitespace-free complement — same author-side intent,
3941        // written in the canonical form the renderer would emit,
3942        // deserializes cleanly.
3943        let json = r#"{"wallClock":"30s"}"#;
3944        let l: LimitsSpec = serde_json::from_str(json).unwrap();
3945        assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
3946    }
3947
3948    // ── canonical-form: non-ASCII Unicode `White_Space` duration gate ─────
3949    //
3950    // Successor to the `parse_duration` ASCII-whitespace arm (ebc3a75)
3951    // — closes the strictly-complementary class the byte-scan cannot
3952    // see, through the lifted
3953    // [`crate::render::find_non_ascii_whitespace_char`] predicate.
3954
3955    #[test]
3956    fn parse_duration_rejects_leading_nbsp() {
3957        // NBSP prefix — paste-from-typography footgun. Byte-scan misses,
3958        // `str::trim` strips silently, drifting to `"30s"` on next
3959        // emit.
3960        let s = "\u{00A0}30s";
3961        let err = parse_duration(s).unwrap_err();
3962        assert!(
3963            matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
3964            "got {err:?}"
3965        );
3966        let msg = err.to_string();
3967        assert!(
3968            msg.contains("U+00A0"),
3969            "diagnostic must name codepoint (got {msg:?})"
3970        );
3971    }
3972
3973    #[test]
3974    fn parse_duration_rejects_internal_em_space() {
3975        // EM-SPACE (`\u{2003}`) between magnitude and unit — canonical
3976        // paste-from-typography footgun on the `<integer><unit>` shape.
3977        let s = "30\u{2003}s";
3978        let err = parse_duration(s).unwrap_err();
3979        assert!(
3980            matches!(err, LimitsError::NonAsciiWhitespaceInDuration { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
3981            "got {err:?}"
3982        );
3983    }
3984
3985    #[test]
3986    fn parse_duration_accepts_ascii_only_canonical_forms_after_unicode_arm() {
3987        // Positive-control pin: every ASCII-only canonical form the
3988        // renderer emits stays accepted through the new arm.
3989        assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
3990        assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
3991        assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
3992    }
3993
3994    #[test]
3995    fn de_duration_rejects_leading_zero_through_serde() {
3996        // The serde-path pin: a `:limits :wall-clock` carrying a
3997        // leading-zero magnitude (`"030s"`) must fail at deserialize
3998        // time, not silently round-trip the value through the parser.
3999        // The gate fires at deserialize, before any validate gate runs
4000        // — peer with the existing `de_duration_rejects_fractional_value_through_serde`
4001        // pin on the same canonical-form-drift axis.
4002        let json = r#"{"wallClock":"030s"}"#;
4003        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4004        let msg = err.to_string();
4005        assert!(
4006            msg.contains("leading zero"),
4007            "serde diagnostic must surface the leading-zero reason verbatim (got {msg:?})"
4008        );
4009
4010        let json = r#"{"wallClock":"30s"}"#;
4011        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4012        assert_eq!(l.wall_clock, Some(Duration::from_secs(30)));
4013    }
4014
4015    #[test]
4016    fn de_duration_rejects_fractional_value_through_serde() {
4017        // The serde-path pin: a `:limits :wall-clock` carrying a
4018        // fractional magnitude (`"1.5s"`) must fail at deserialize time,
4019        // not silently round-trip the value through the f64 parser. Pin
4020        // both the success-on-canonical path (the integer form
4021        // deserializes cleanly) and the failure-on-non-canonical path
4022        // (the fractional form is rejected by the codec before any
4023        // validate gate runs).
4024        let json = r#"{"wallClock":"1.5s"}"#;
4025        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4026        let msg = err.to_string();
4027        assert!(
4028            msg.contains("non-negative integer"),
4029            "serde diagnostic must surface the integer-magnitude reason verbatim \
4030             (got {msg:?})"
4031        );
4032
4033        // The integer-form complement — same author-side intent
4034        // (1.5s = 1500ms), written in the canonical form the renderer
4035        // would emit, deserializes cleanly.
4036        let json = r#"{"wallClock":"1500ms"}"#;
4037        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4038        assert_eq!(l.wall_clock, Some(Duration::from_millis(1500)));
4039    }
4040
4041    #[test]
4042    fn de_byte_size_rejects_fractional_value_through_serde() {
4043        // The serde-path pin: a `:limits :memory` carrying a fractional
4044        // magnitude (`"1.5KiB"`) must fail at deserialize time, not
4045        // silently round-trip the value through the f64 parser. Pin
4046        // both the success-on-canonical path (the integer form
4047        // deserializes cleanly) and the failure-on-non-canonical path
4048        // (the fractional form is rejected by the codec before any
4049        // validate gate runs).
4050        let json = r#"{"memory":"1.5KiB"}"#;
4051        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4052        let msg = err.to_string();
4053        assert!(
4054            msg.contains("non-negative integer"),
4055            "serde diagnostic must surface the integer-magnitude reason verbatim (got {msg:?})"
4056        );
4057
4058        // The integer-form complement — same author-side intent
4059        // (1.5KiB = 1536 bytes), written in the canonical form the
4060        // renderer would emit, deserializes cleanly.
4061        let json = r#"{"memory":"1536"}"#;
4062        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4063        assert_eq!(l.memory, Some(1536));
4064    }
4065
4066    // ── canonical-form: integer-magnitude millicores codec gate ───────────
4067    //
4068    // Direct successor to the `parse_byte_size` / `parse_duration` /
4069    // shared `supervisor::duration_codec` / `rate_limit_codec`
4070    // integer-magnitude gates on the four peer typed codecs in
4071    // caixa-core — closes the sixth (and last) typed-codec surface in
4072    // the crate. Every magnitude `render_millicores` emits is a
4073    // non-negative integer (`format!("{m}m")`) — no decimal point, no
4074    // leading sign, no scientific notation. The parser's accepted set
4075    // must match for parse → render → parse to round-trip without
4076    // canonical-form drift. Pins every canonical-drift shape —
4077    // leading-`+` (`"+500m"` / `"+2"`, the load-bearing class the
4078    // digit-only gate closes beyond `u32::from_str` strictness),
4079    // leading-`-` (`"-100m"`), fractional (`"1.5"`), decimal-shaped-
4080    // integer on both authoring paths (`"500.0m"` / `"2.0"`), the
4081    // bare-`m`-with-no-magnitude pin, the empty-string pin, the
4082    // garbage-precedence pin (genuinely unparseable inputs keep the
4083    // narrower `BadMillicores` diagnostic), the u32-overflow surface
4084    // pin on both the `m`-suffix and bare-core multiply paths, the
4085    // complement-side pin (every integer happy path the gate must
4086    // continue to accept), the round-trip convergence property, and
4087    // the serde-path pin (the gate fires at deserialize, before any
4088    // validate gate runs).
4089
4090    #[test]
4091    fn parse_millicores_rejects_fractional_magnitude() {
4092        // The fail-before-pass-after pin on the bare-core path:
4093        // `"1.5"` parsed cleanly on no pre-gate codebase (`u32::from_str`
4094        // rejects the decimal), but the diagnostic was value-laundered
4095        // (the bare `BadMillicores("1.5")` wording didn't name the
4096        // canonical-form remediation or the round-trip drift the next
4097        // emit would produce — `1.5 cores × 1000 = 1500 millicores` →
4098        // `"1500m"` on the renderer). The gate routes the same input to
4099        // `NonIntegerMillicoreMagnitude` with the canonical-form wording.
4100        let err = parse_millicores("1.5").unwrap_err();
4101        assert!(
4102            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "1.5"),
4103            "got {err:?}"
4104        );
4105    }
4106
4107    #[test]
4108    fn parse_millicores_rejects_decimal_shaped_integer_with_suffix() {
4109        // The canonical-drift case on the `m`-suffix path where the
4110        // *value* is integer but the *form* carries a redundant decimal
4111        // point — `"500.0m"` parses to 500 millicores (integer), but
4112        // the renderer emits `"500m"` on the next serialize (no decimal
4113        // point). The parse-shape gate fires here too so the codec's
4114        // accepted set is exactly the renderer's emitted set — same
4115        // shape as `parse_byte_size`'s `"1.0MiB"` case.
4116        let err = parse_millicores("500.0m").unwrap_err();
4117        assert!(
4118            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "500.0"),
4119            "got {err:?}"
4120        );
4121    }
4122
4123    #[test]
4124    fn parse_millicores_rejects_decimal_shaped_integer_bare_core() {
4125        // The decimal-shaped-integer pin on the bare-core path —
4126        // `"2.0"` would be 2000 millicores (the canonical `"2000m"`),
4127        // but the redundant decimal point is not a renderer-emitted
4128        // shape. Surfaces under the same diagnostic as the `m`-suffix
4129        // path so the gate's coverage is uniform across both authoring
4130        // paths.
4131        let err = parse_millicores("2.0").unwrap_err();
4132        assert!(
4133            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "2.0"),
4134            "got {err:?}"
4135        );
4136    }
4137
4138    #[test]
4139    fn parse_millicores_rejects_leading_plus_sign_with_suffix() {
4140        // The load-bearing class the digit-only gate closes beyond
4141        // `u32::from_str`'s strictness: current Rust `u32::from_str`
4142        // permissively accepts `"+500"` → 500, so `"+500m"` parsed
4143        // cleanly through the pre-gate codec to `RateLimit`-shaped
4144        // 500 millicores and serde silently round-tripped to `"500m"`
4145        // on the next emit — a *different* canonical string. Same
4146        // shape as `parse_byte_size`'s `"+1024"` (875 commit) and
4147        // `parse_duration`'s `"+30s"` (1027 commit) cases on the peer
4148        // codecs.
4149        let err = parse_millicores("+500m").unwrap_err();
4150        assert!(
4151            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+500"),
4152            "got {err:?}"
4153        );
4154    }
4155
4156    #[test]
4157    fn parse_millicores_rejects_leading_plus_sign_bare_core() {
4158        // The leading-`+` pin on the bare-core path — `"+2"` parsed
4159        // through `u32::from_str` as 2 → 2000 millicores → `"2000m"`
4160        // on the renderer; canonical-drift. The digit-only gate routes
4161        // the same input to `NonIntegerMillicoreMagnitude`, peer with
4162        // the `m`-suffix path.
4163        let err = parse_millicores("+2").unwrap_err();
4164        assert!(
4165            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "+2"),
4166            "got {err:?}"
4167        );
4168    }
4169
4170    #[test]
4171    fn parse_millicores_rejects_leading_minus_sign() {
4172        // The negative-magnitude class — pre-gate `u32::from_str`
4173        // rejected negatives but the diagnostic collapsed onto the
4174        // opaque `BadMillicores("-100m")` wording. The digit-only gate
4175        // fires earlier and routes the same input to
4176        // `NonIntegerMillicoreMagnitude` (negatives are not digit-only,
4177        // and `i64::from_str` accepts the leading sign so the numeric
4178        // arm matches). Pin the new diagnostic so a future relaxation
4179        // that re-routes negatives back to the old arm surfaces here.
4180        let err = parse_millicores("-100m").unwrap_err();
4181        assert!(
4182            matches!(err, LimitsError::NonIntegerMillicoreMagnitude { ref value } if value == "-100"),
4183            "got {err:?}"
4184        );
4185    }
4186
4187    #[test]
4188    fn parse_millicores_rejects_empty_string() {
4189        // The empty-input pin — `""` is not a magnitude at all. Pre-
4190        // gate this fell through to `s.parse::<u32>()` and surfaced as
4191        // a generic parse failure with the same `BadMillicores("")`
4192        // wording; the explicit empty-check at the top of the codec
4193        // surfaces the same diagnostic earlier and makes the empty-
4194        // input class structurally distinct from the digit-only /
4195        // numeric / garbage arms below.
4196        let err = parse_millicores("").unwrap_err();
4197        assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4198    }
4199
4200    #[test]
4201    fn parse_millicores_rejects_bare_unit_with_no_magnitude() {
4202        // The bare-`m`-with-no-magnitude pin — `"m"` strips to `""`,
4203        // which is not a magnitude at all. The canonical millicores
4204        // authoring form requires a magnitude in front of the unit
4205        // (`"500m"`, not `"m"`). Surface as `BadMillicores` so the
4206        // narrower-arm wording stays load-bearing for this class.
4207        let err = parse_millicores("m").unwrap_err();
4208        assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4209    }
4210
4211    #[test]
4212    fn parse_millicores_garbage_still_falls_through_to_bad_millicores() {
4213        // The precedence pin: the new `NonIntegerMillicoreMagnitude`
4214        // arm distinguishes *non-canonical-but-numeric* (`"1.5"`,
4215        // `"+500m"`, `"-100m"`, `"500.0m"`) from *genuinely-
4216        // unparseable* (`"abc"`, `"--1m"`, `"foo"`) so the existing
4217        // `BadMillicores` diagnostic's wording remains load-bearing
4218        // for the latter class — the gate is additive, not replacing.
4219        // Pin both arms so a future relaxation that collapses them
4220        // surfaces here.
4221        let err = parse_millicores("abc").unwrap_err();
4222        assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4223        let err = parse_millicores("--1m").unwrap_err();
4224        assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4225        let err = parse_millicores("foo").unwrap_err();
4226        assert!(matches!(err, LimitsError::BadMillicores(_)), "got {err:?}");
4227    }
4228
4229    #[test]
4230    fn parse_millicores_u32_overflow_with_suffix_surfaces_as_overflow() {
4231        // The u32-overflow surface pin on the `m`-suffix path: a
4232        // magnitude exceeding `u32::MAX` (4294967296 = u32::MAX + 1)
4233        // surfaces as `BadMillicores` with an overflow-shaped wording
4234        // naming the offending magnitude verbatim. The digit-only
4235        // guard guarantees every byte is `[0-9]`, so overflow is the
4236        // only remaining `u32::from_str` failure mode — the overflow
4237        // arm is no longer in unreachable-by-prior-gate territory.
4238        // Matches the overflow-arm shape on `parse_byte_size` /
4239        // `parse_duration` / `rate_limit_codec`.
4240        let err = parse_millicores("4294967296m").unwrap_err();
4241        let LimitsError::BadMillicores(reason) = err else {
4242            panic!("expected BadMillicores(overflow), got other variant");
4243        };
4244        assert!(
4245            reason.contains("overflow"),
4246            "overflow diagnostic must mention overflow (got {reason:?})"
4247        );
4248    }
4249
4250    #[test]
4251    fn parse_millicores_bare_core_overflow_surfaces_as_overflow() {
4252        // The u32-overflow surface pin on the bare-core path: a
4253        // magnitude that fits u32 on its own but overflows on the
4254        // `× 1000` conversion to millicores surfaces as
4255        // `BadMillicores` with an overflow-shaped wording. Pre-gate
4256        // the codec used `saturating_mul(1000)` which silently
4257        // saturated the result at `u32::MAX` — landing as the cap
4258        // value far from the author's intent and bypassing any
4259        // future validate-time upper-bound gate the `:cpu` axis
4260        // grows. The `checked_mul` rewrite surfaces the overflow at
4261        // parse time. (4294968 cores × 1000 = 4294968000 > u32::MAX
4262        // = 4294967295 — the smallest digit-string that overflows
4263        // u32 on the × 1000 multiply while fitting u32 on its own.)
4264        let err = parse_millicores("4294968").unwrap_err();
4265        let LimitsError::BadMillicores(reason) = err else {
4266            panic!("expected BadMillicores(× 1000 overflow), got other variant");
4267        };
4268        assert!(
4269            reason.contains("overflow"),
4270            "× 1000 overflow diagnostic must mention overflow (got {reason:?})"
4271        );
4272    }
4273
4274    #[test]
4275    fn parse_millicores_continues_to_accept_canonical_forms() {
4276        // The complement-side pin: every canonical integer-magnitude
4277        // form the renderer emits must continue to parse to the same
4278        // value the renderer produced. Sweep the canonical authoring
4279        // shapes on both paths (the `m`-suffix path: `"0m"`, `"500m"`,
4280        // `"2000m"`; the bare-core shorthand: `"0"`, `"2"`, `"4"`) so
4281        // a future tightening of the parser surfaces here as a test
4282        // failure rather than a silent regression. The `0` case is at
4283        // the codec layer only; `validate_rejects_zero_cpu` rejects
4284        // `Some(0)` one level up.
4285        assert_eq!(parse_millicores("0m").unwrap(), 0);
4286        assert_eq!(parse_millicores("500m").unwrap(), 500);
4287        assert_eq!(parse_millicores("1500m").unwrap(), 1500);
4288        assert_eq!(parse_millicores("2000m").unwrap(), 2000);
4289        assert_eq!(parse_millicores("0").unwrap(), 0);
4290        assert_eq!(parse_millicores("2").unwrap(), 2000);
4291        assert_eq!(parse_millicores("4").unwrap(), 4000);
4292    }
4293
4294    #[test]
4295    fn parse_millicores_round_trips_through_render_for_every_canonical_form() {
4296        // The structural property the gate makes load-bearing: every
4297        // value the parser accepts round-trips through
4298        // `render_millicores` to a string the parser also accepts —
4299        // and to the *same* value. Sweep the values the renderer emits
4300        // canonically (zero, sub-core, single-core boundary, multi-
4301        // core, and a non-1000-multiple millicore value) so a future
4302        // codec change that breaks round-trip convergence surfaces
4303        // here, not at a downstream renderer that double-emits a
4304        // typed slot.
4305        for m in [0u32, 1, 100, 500, 1000, 1500, 2000, 12345] {
4306            let rendered = render_millicores(m);
4307            let reparsed = parse_millicores(&rendered)
4308                .unwrap_or_else(|e| panic!("render({m}) = {rendered:?} must reparse, got {e:?}"));
4309            assert_eq!(
4310                reparsed, m,
4311                "round-trip drift on {m}: rendered={rendered:?}, reparsed={reparsed}",
4312            );
4313        }
4314    }
4315
4316    #[test]
4317    fn de_millicores_rejects_leading_plus_through_serde() {
4318        // The serde-path pin: a `:limits :cpu` carrying a leading-`+`
4319        // magnitude (`"+500m"`) must fail at deserialize time, not
4320        // silently round-trip the value through `u32::from_str`'s
4321        // permissive sign-acceptance. Pin both the success-on-canonical
4322        // path (the integer form deserializes cleanly) and the
4323        // failure-on-non-canonical path (the leading-`+` form is
4324        // rejected by the codec before any validate gate runs).
4325        let json = r#"{"cpu":"+500m"}"#;
4326        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4327        let msg = err.to_string();
4328        assert!(
4329            msg.contains("non-negative integer"),
4330            "serde diagnostic must surface the integer-magnitude reason verbatim \
4331             (got {msg:?})"
4332        );
4333
4334        // The integer-form complement — same author-side intent
4335        // (500 millicores), written in the canonical form the renderer
4336        // would emit, deserializes cleanly.
4337        let json = r#"{"cpu":"500m"}"#;
4338        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4339        assert_eq!(l.cpu, Some(500));
4340    }
4341
4342    // ── canonical-form: leading-zero millicores codec gate ────────────────
4343    //
4344    // Direct successor to the `parse_byte_size` / `parse_duration` /
4345    // `supervisor::duration_codec` / `rate_limit_codec` leading-zero
4346    // arms (cea9a78 / 39762d7 / 9178904 / 4f46830) — closes the sixth
4347    // (and last) typed numeric-codec surface in caixa-core on the
4348    // integer-magnitude leading-zero axis. Every magnitude
4349    // `render_millicores` emits is the leading-zero-stripped form
4350    // (`format!("{m}m")` — no leading-zero padding), so a digit-only-
4351    // but-leading-zero magnitude parses losslessly through `u32::from_str`
4352    // and serde silently round-trips the value to a *different*
4353    // canonical string on the next emit. Pins every canonical-drift
4354    // shape on the `m`-suffix and bare-core paths, the codec-vs-
4355    // typed-validate-layer boundary (the single-byte `"0"` stays in the
4356    // codec's accepted set; `CpuZero` refuses it at validate), the
4357    // complement-side pin (every canonical leading-`[1-9]` magnitude
4358    // continues to parse cleanly), and the serde-path pin.
4359
4360    #[test]
4361    fn parse_millicores_rejects_leading_zero_magnitude_with_suffix() {
4362        // The fail-before-pass-after pin on the `m`-suffix path:
4363        // `"0500m"` parsed cleanly on no pre-gate codebase
4364        // (`u32::from_str` accepts `"0500"` → 500), then `render_millicores`
4365        // emitted `"500m"` on the next serialize — canonical-form drift.
4366        // The leading-zero arm routes the same input to
4367        // `LeadingZeroMillicoreMagnitude` with the canonical-form
4368        // remediation wording. Peer with the `parse_byte_size` `"064MiB"`
4369        // case and the `parse_duration` `"030s"` case.
4370        let err = parse_millicores("0500m").unwrap_err();
4371        assert!(
4372            matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "0500"),
4373            "got {err:?}"
4374        );
4375    }
4376
4377    #[test]
4378    fn parse_millicores_rejects_multi_digit_zero_magnitude_with_suffix() {
4379        // The multi-zero pin on the `m`-suffix path: `"00m"` parses to 0
4380        // millicores at the codec, but the renderer emits `"0m"` on the
4381        // next serialize — the single canonical zero form on this axis.
4382        // The leading-zero arm rejects multi-byte leading-zero shapes
4383        // even when the value is zero; the single-byte `"0m"` /
4384        // bare-`"0"` stays in the codec's accepted set per the boundary
4385        // pin below. Peer with the `parse_byte_size` `"00MiB"` case and
4386        // the `parse_duration` `"00s"` case.
4387        let err = parse_millicores("00m").unwrap_err();
4388        assert!(
4389            matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "00"),
4390            "got {err:?}"
4391        );
4392    }
4393
4394    #[test]
4395    fn parse_millicores_rejects_leading_zero_bare_core() {
4396        // The leading-zero pin on the bare-core path: `"02"` parsed to
4397        // 2 cores → 2000 millicores at the codec, but `render_millicores`
4398        // emits `"2000m"` on the next serialize — canonical-form drift.
4399        // The bare-core shorthand carries the same leading-zero discipline
4400        // as the `m`-suffix path; both authoring paths converge to the
4401        // same gate. Peer with the `parse_byte_size` bare-integer
4402        // `"01024"` case.
4403        let err = parse_millicores("02").unwrap_err();
4404        assert!(
4405            matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "02"),
4406            "got {err:?}"
4407        );
4408    }
4409
4410    #[test]
4411    fn parse_millicores_rejects_leading_zero_multi_digit_with_suffix() {
4412        // The multi-digit leading-zero pin on the `m`-suffix path:
4413        // `"01500m"` parses to 1500 millicores at the codec, but the
4414        // renderer emits `"1500m"` on the next serialize — canonical-form
4415        // drift on a non-zero magnitude. Sweeps a different magnitude
4416        // shape than the `"0500m"` case so a future tightening that
4417        // misses the multi-digit-leading-zero class surfaces here.
4418        let err = parse_millicores("01500m").unwrap_err();
4419        assert!(
4420            matches!(err, LimitsError::LeadingZeroMillicoreMagnitude { ref value } if value == "01500"),
4421            "got {err:?}"
4422        );
4423    }
4424
4425    #[test]
4426    fn parse_millicores_accepts_single_zero_magnitude_at_codec_layer() {
4427        // The codec-layer / typed-validate-layer boundary pin: the
4428        // single-byte magnitude `"0"` (bare) and `"0m"` (with suffix)
4429        // round-trip losslessly through `render_millicores` (which
4430        // emits `"0m"` for 0 millicores), so they stay in the codec's
4431        // accepted set. The downstream `CpuZero` gate refuses
4432        // semantic-zero authoring at the typed-validate layer above —
4433        // the diagnostic partitioning between canonical-form drift
4434        // (the leading-zero arm) and semantic-zero (the `CpuZero` gate)
4435        // remains stable. Same codec-layer / typed-validate-layer
4436        // partition the peer codecs preserve.
4437        assert_eq!(parse_millicores("0").unwrap(), 0);
4438        assert_eq!(parse_millicores("0m").unwrap(), 0);
4439    }
4440
4441    #[test]
4442    fn parse_millicores_accepts_canonical_magnitude_with_leading_one() {
4443        // The complement-side pin: every canonical leading-`[1-9]`
4444        // magnitude continues to parse cleanly through the leading-zero
4445        // arm, on both the `m`-suffix and bare-core paths. Sweep the
4446        // canonical values the renderer emits across the unit-multiplier
4447        // boundary (sub-core, single-core, multi-core) so a future
4448        // tightening cannot drift into rejecting valid canonical
4449        // magnitudes. Same complement-side discipline the peer
4450        // `parse_byte_size_accepts_canonical_magnitude_with_leading_one`
4451        // and `parse_duration_accepts_canonical_magnitude_with_leading_one`
4452        // pins enforce on the sibling codecs.
4453        assert_eq!(parse_millicores("1m").unwrap(), 1);
4454        assert_eq!(parse_millicores("500m").unwrap(), 500);
4455        assert_eq!(parse_millicores("1500m").unwrap(), 1500);
4456        assert_eq!(parse_millicores("9000m").unwrap(), 9000);
4457        assert_eq!(parse_millicores("1").unwrap(), 1000);
4458        assert_eq!(parse_millicores("2").unwrap(), 2000);
4459        assert_eq!(parse_millicores("9").unwrap(), 9000);
4460    }
4461
4462    #[test]
4463    fn de_millicores_rejects_leading_zero_through_serde() {
4464        // The serde-path pin: a `:limits :cpu` carrying a leading-zero
4465        // magnitude (`"0500m"`) must fail at deserialize time, not
4466        // silently round-trip the value through `u32::from_str`'s
4467        // leading-zero-permissive accepting. Pin both the success-on-
4468        // canonical path (the leading-zero-stripped form deserializes
4469        // cleanly) and the failure-on-non-canonical path (the leading-
4470        // zero form is rejected by the codec before any validate gate
4471        // runs). Peer with the
4472        // `de_byte_size_rejects_leading_zero_through_serde` and
4473        // `de_duration_rejects_leading_zero_through_serde` pins on the
4474        // sibling codecs.
4475        let json = r#"{"cpu":"0500m"}"#;
4476        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4477        let msg = err.to_string();
4478        assert!(
4479            msg.contains("leading zero"),
4480            "serde diagnostic must surface the leading-zero reason verbatim \
4481             (got {msg:?})"
4482        );
4483
4484        // The integer-form complement — same author-side intent
4485        // (500 millicores), written in the canonical form the renderer
4486        // would emit, deserializes cleanly.
4487        let json = r#"{"cpu":"500m"}"#;
4488        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4489        assert_eq!(l.cpu, Some(500));
4490    }
4491
4492    // ── canonical-form: whitespace-rejection millicores codec gate ────────
4493    //
4494    // Direct successor to the `parse_byte_size` (24a8ad4), `parse_duration`
4495    // (ebc3a75), `supervisor::duration_codec` (a7ae622), and
4496    // `rate_limit_codec` (1ad7755) whitespace-rejection arms — closes the
4497    // fifth (and last) typed-magnitude codec surface in caixa-core on the
4498    // ASCII-whitespace axis. The pre-gate top-level `s.trim()` at parse
4499    // entry and the per-part `magnitude.trim()` calls silently ate leading
4500    // / trailing / internal whitespace, so every whitespace-carrying shape
4501    // parsed to the same millicore value and round-tripped through
4502    // `render_millicores` to a *different* canonical string on next
4503    // serialize — the same canonical-form-drift class the leading-`+` /
4504    // fractional / leading-zero arms already close on this codec.
4505
4506    #[test]
4507    fn parse_millicores_rejects_leading_whitespace() {
4508        // `" 500m"` — the canonical paste-from-aligned-doc / YAML-quoted-
4509        // plain-scalar footgun. Before this gate the top-level `s.trim()`
4510        // at parse entry silently ate the leading space and parsed the
4511        // value to 500 millicores, round-tripping to `"500m"` on next
4512        // serialize.
4513        let err = parse_millicores(" 500m").unwrap_err();
4514        assert!(
4515            matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == " 500m" && byte == 0x20),
4516            "got {err:?}"
4517        );
4518        let msg = err.to_string();
4519        assert!(
4520            msg.contains("whitespace byte 0x20"),
4521            "diagnostic must surface the offending byte verbatim (got {msg:?})"
4522        );
4523        assert!(
4524            msg.contains("THEORY.md"),
4525            "diagnostic must cite the render-determinism contract (got {msg:?})"
4526        );
4527    }
4528
4529    #[test]
4530    fn parse_millicores_rejects_trailing_whitespace() {
4531        // `"500m "` — the canonical shell-history trailing-space footgun.
4532        let err = parse_millicores("500m ").unwrap_err();
4533        assert!(
4534            matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m " && byte == 0x20),
4535            "got {err:?}"
4536        );
4537    }
4538
4539    #[test]
4540    fn parse_millicores_rejects_internal_whitespace_between_magnitude_and_unit() {
4541        // `"500 m"` — the typographically-spaced author shape (the same
4542        // idiom every prose reference to millicores renders as). Before
4543        // this gate the per-part `magnitude.trim()` silently ate the
4544        // internal space and parsed the value to 500 millicores.
4545        let err = parse_millicores("500 m").unwrap_err();
4546        assert!(
4547            matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500 m" && byte == 0x20),
4548            "got {err:?}"
4549        );
4550    }
4551
4552    #[test]
4553    fn parse_millicores_rejects_tab_byte() {
4554        // `"\t500m"` — the paste-from-indented-doc / YAML-block-scalar tab
4555        // footgun. Pins the tab (`0x09`) arm alongside the space arm above.
4556        let err = parse_millicores("\t500m").unwrap_err();
4557        assert!(
4558            matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "\t500m" && byte == 0x09),
4559            "got {err:?}"
4560        );
4561    }
4562
4563    #[test]
4564    fn parse_millicores_rejects_trailing_newline() {
4565        // `"500m\n"` — the multi-line-paste footgun where a trailing LF
4566        // byte survives the paste. Pins the LF member (`0x0A`) of the
4567        // `is_ascii_whitespace` set.
4568        let err = parse_millicores("500m\n").unwrap_err();
4569        assert!(
4570            matches!(err, LimitsError::WhitespaceInMillicores { ref value, byte } if value == "500m\n" && byte == 0x0a),
4571            "got {err:?}"
4572        );
4573    }
4574
4575    #[test]
4576    fn parse_millicores_accepts_whitespace_free_canonical_forms() {
4577        // The complement-side pin: every canonical whitespace-free
4578        // authoring form the renderer emits stays accepted post-gate.
4579        // Sweep the canonical `m`-suffix path plus the bare-core shorthand
4580        // so a future tightening of the whitespace arm that over-fires on
4581        // the accepted set surfaces here as a test failure.
4582        assert_eq!(parse_millicores("500m").unwrap(), 500);
4583        assert_eq!(parse_millicores("2000m").unwrap(), 2000);
4584        assert_eq!(parse_millicores("1m").unwrap(), 1);
4585        assert_eq!(parse_millicores("0m").unwrap(), 0);
4586        assert_eq!(parse_millicores("2").unwrap(), 2000);
4587        assert_eq!(parse_millicores("0").unwrap(), 0);
4588    }
4589
4590    #[test]
4591    fn de_millicores_rejects_whitespace_through_serde() {
4592        // The serde-path pin: a `:limits :cpu` carrying a whitespace-byte-
4593        // carrying value (`" 500m"`) must fail at deserialize time, not
4594        // silently round-trip the value through the pre-existing top-level
4595        // `s.trim()`. Peer with the
4596        // `de_byte_size_rejects_whitespace_through_serde` and
4597        // `de_duration_rejects_whitespace_through_serde` pins on the
4598        // sibling codecs.
4599        let json = r#"{"cpu":" 500m"}"#;
4600        let err = serde_json::from_str::<LimitsSpec>(json).unwrap_err();
4601        let msg = err.to_string();
4602        assert!(
4603            msg.contains("whitespace byte"),
4604            "serde diagnostic must surface the whitespace reason verbatim (got {msg:?})"
4605        );
4606        assert!(
4607            msg.contains("0x20"),
4608            "serde diagnostic must name the offending byte (got {msg:?})"
4609        );
4610
4611        // The whitespace-free complement — same author-side intent,
4612        // written in the canonical form the renderer would emit,
4613        // deserializes cleanly.
4614        let json = r#"{"cpu":"500m"}"#;
4615        let l: LimitsSpec = serde_json::from_str(json).unwrap();
4616        assert_eq!(l.cpu, Some(500));
4617    }
4618
4619    // ── canonical-form: non-ASCII Unicode `White_Space` millicores gate ───
4620    //
4621    // Direct successor to the ASCII-whitespace arm above — closes the
4622    // strictly-complementary class the byte-scan cannot see. `str::trim`
4623    // uses `char::is_whitespace` (Unicode `White_Space`, strictly wider
4624    // than the ASCII byte set); a leading / trailing / internal NBSP
4625    // (`\u{00A0}`) / LINE SEPARATOR (`\u{2028}`) / EM-SPACE (`\u{2003}`)
4626    // survives the byte-scan but is silently stripped by the top-level
4627    // trim, drifting to canonical `"500m"` on round-trip. Pins the arm
4628    // through the lifted [`crate::render::find_non_ascii_whitespace_char`]
4629    // predicate — the same shared predicate 1b75b38 landed on the four
4630    // peer typed-magnitude codecs, extended here to the fifth.
4631
4632    #[test]
4633    fn parse_millicores_rejects_leading_nbsp() {
4634        // NBSP (`\u{00A0}` = UTF-8 `0xC2 0xA0`) — the paste-from-typography
4635        // / paste-from-word-processor footgun. Before this arm landed the
4636        // byte-scan missed it (neither `0xC2` nor `0xA0` is
4637        // `is_ascii_whitespace`) and `str::trim` at parse entry silently
4638        // stripped it, yielding the same 500 millicores as the whitespace-
4639        // free canonical form and drifting to `"500m"` on next serialize.
4640        let s = "\u{00A0}500m";
4641        let err = parse_millicores(s).unwrap_err();
4642        assert!(
4643            matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{00A0}' && codepoint == 0x00A0),
4644            "got {err:?}"
4645        );
4646        let msg = err.to_string();
4647        assert!(
4648            msg.contains("U+00A0"),
4649            "diagnostic must surface the codepoint verbatim (got {msg:?})"
4650        );
4651        assert!(
4652            msg.contains("THEORY.md"),
4653            "diagnostic must cite the render-determinism contract (got {msg:?})"
4654        );
4655    }
4656
4657    #[test]
4658    fn parse_millicores_rejects_internal_em_space() {
4659        // EM-SPACE (`\u{2003}`) between magnitude and unit — pins the arm
4660        // on an internal-position non-NBSP Unicode `White_Space` member.
4661        let s = "500\u{2003}m";
4662        let err = parse_millicores(s).unwrap_err();
4663        assert!(
4664            matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2003}' && codepoint == 0x2003),
4665            "got {err:?}"
4666        );
4667    }
4668
4669    #[test]
4670    fn parse_millicores_rejects_trailing_line_separator() {
4671        // LINE SEPARATOR (`\u{2028}`) — the canonical paste-from-web-doc
4672        // footgun (many rendering engines insert `\u{2028}` at soft-wrap
4673        // boundaries in RTF/HTML → plain text conversion). Pins the arm on
4674        // a trailing-position Unicode `White_Space` member.
4675        let s = "500m\u{2028}";
4676        let err = parse_millicores(s).unwrap_err();
4677        assert!(
4678            matches!(err, LimitsError::NonAsciiWhitespaceInMillicores { ref value, ch, codepoint } if value == s && ch == '\u{2028}' && codepoint == 0x2028),
4679            "got {err:?}"
4680        );
4681    }
4682
4683    #[test]
4684    fn parse_millicores_accepts_ascii_only_canonical_forms_after_unicode_arm() {
4685        // Positive-control pin: every ASCII-only canonical form the
4686        // renderer emits stays accepted through the new arm — the lifted
4687        // predicate is a strict no-op on ASCII input.
4688        assert_eq!(parse_millicores("500m").unwrap(), 500);
4689        assert_eq!(parse_millicores("2000m").unwrap(), 2000);
4690        assert_eq!(parse_millicores("1m").unwrap(), 1);
4691        assert_eq!(parse_millicores("2").unwrap(), 2000);
4692    }
4693
4694    // ── canonical-form: integer-millisecond :wall-clock gate ──────────────
4695    //
4696    // The peer typed-`Duration` axes routed through
4697    // `supervisor::duration_codec` (`:politicas :timeout` a4ae535,
4698    // `:circuit-breaker :window` a4ae535) already gate on
4699    // `is_integer_millisecond_duration` because the codec's `render`
4700    // truncates to `as_millis()` and parses with integer-ms granularity;
4701    // this crate's in-module `render_duration` / `parse_duration` pair
4702    // carries the same `as_millis()`-truncation shape, so the same sub-
4703    // millisecond-residue footgun lived on this axis until this gate
4704    // landed. The tests below pin the fail-before-pass-after boundary,
4705    // the diagnostic shape, the cross-arm zero-then-canonical ordering
4706    // matching the `:politicas` peer, the integer-ms happy-path sweep,
4707    // and the codec round-trip property (every validated `wall_clock`
4708    // survives serialize → deserialize equality).
4709
4710    #[test]
4711    fn validate_rejects_sub_millisecond_wall_clock() {
4712        // The fail-before-pass-after pin: a programmatic
4713        // `Duration::from_micros(1500)` (= 1_500_000 ns) silently passed
4714        // validate on every pre-gate codebase, then truncated to
4715        // `as_millis() == 1` on first serialize — `render_duration`
4716        // emits `"1ms"`, the codec parses it back to
4717        // `Duration::from_millis(1)` = 1_000_000 ns, the typed
4718        // `wall_clock` no longer matches its rendered form.
4719        let l = LimitsSpec {
4720            wall_clock: Some(Duration::from_micros(1500)),
4721            ..Default::default()
4722        };
4723        match l.validate().unwrap_err() {
4724            LimitsError::WallClockNotCanonical { wall_clock } => {
4725                assert_eq!(wall_clock, Duration::from_micros(1500));
4726            }
4727            other => panic!("expected WallClockNotCanonical, got {other:?}"),
4728        }
4729    }
4730
4731    #[test]
4732    fn validate_rejects_one_nanosecond_wall_clock() {
4733        // The far-sub-ms case: `Duration::from_nanos(1)` is non-zero
4734        // (so `WallClockZero` doesn't fire) but `as_millis() == 0`, so
4735        // `render_duration` emits the literal `"0s"` — the next serde
4736        // round-trip would parse back to `Duration::ZERO`, which the
4737        // `WallClockZero` arm then rejects on re-validate. The
4738        // canonical-form gate at this layer surfaces a self-locating
4739        // diagnostic naming the offending Duration verbatim rather
4740        // than a downstream `WallClockZero` whose remediation points
4741        // at omitting the slot.
4742        let l = LimitsSpec {
4743            wall_clock: Some(Duration::from_nanos(1)),
4744            ..Default::default()
4745        };
4746        match l.validate().unwrap_err() {
4747            LimitsError::WallClockNotCanonical { wall_clock } => {
4748                assert_eq!(wall_clock, Duration::from_nanos(1));
4749            }
4750            other => panic!("expected WallClockNotCanonical, got {other:?}"),
4751        }
4752    }
4753
4754    #[test]
4755    fn validate_rejects_nanosecond_past_canonical_boundary() {
4756        // The 1-ns-past-1ms boundary case: a `Duration` carrying
4757        // 1_000_001 ns is structurally past the integer-ms granularity
4758        // floor — `subsec_nanos() % 1_000_000 == 1`. The codec
4759        // round-trip would truncate to `1ms` and the consumer would
4760        // observe a 1-ns drift on every emit. Same boundary the peer
4761        // `is_integer_millisecond_duration_predicate_tracks_codec` test
4762        // in aplicacao.rs pins for the `:politicas` axes.
4763        let w = Duration::from_nanos(1_000_001);
4764        let l = LimitsSpec {
4765            wall_clock: Some(w),
4766            ..Default::default()
4767        };
4768        assert_eq!(
4769            l.validate().unwrap_err(),
4770            LimitsError::WallClockNotCanonical { wall_clock: w }
4771        );
4772    }
4773
4774    #[test]
4775    fn validate_accepts_integer_millisecond_wall_clock_values() {
4776        // The positive-control sweep: every `Duration` the codec can
4777        // round-trip losslessly — the canonical `<integer>{ms,s,m,h}`
4778        // set the `render_duration` / `parse_duration` pair emits and
4779        // accepts — passes `validate` without surfacing the new
4780        // canonical-form arm. Mirrors
4781        // `accepts_policy_retries_typical_values` /
4782        // `accepts_circuit_breaker_max_failures_typical_values` on
4783        // sibling axes.
4784        for w in [
4785            Duration::from_millis(1),
4786            Duration::from_millis(500),
4787            Duration::from_millis(1500),
4788            Duration::from_secs(1),
4789            Duration::from_secs(30),
4790            Duration::from_secs(60),
4791            Duration::from_secs(120),
4792            Duration::from_secs(3600),
4793        ] {
4794            let l = LimitsSpec {
4795                wall_clock: Some(w),
4796                ..Default::default()
4797            };
4798            l.validate()
4799                .unwrap_or_else(|e| panic!("integer-ms {w:?} must validate, got {e:?}"));
4800        }
4801    }
4802
4803    #[test]
4804    fn validate_wall_clock_zero_takes_precedence_over_canonical_gate() {
4805        // Cross-arm ordering pin: `Duration::ZERO` has
4806        // `subsec_nanos() == 0` and would otherwise pass the
4807        // canonical-form arm — the zero-floor arm must fire first so
4808        // the more self-locating `WallClockZero` diagnostic (with its
4809        // omit-axis remediation directly named) leads. Same posture
4810        // every peer zero-then-shape gate uses
4811        // (`PolicyTimeoutZero` → `PolicyTimeoutNotCanonical`,
4812        // `PolicyBreakerZeroWindow` → `PolicyBreakerWindowNotCanonical`).
4813        let l = LimitsSpec {
4814            wall_clock: Some(Duration::ZERO),
4815            ..Default::default()
4816        };
4817        assert_eq!(l.validate().unwrap_err(), LimitsError::WallClockZero);
4818    }
4819
4820    #[test]
4821    fn wall_clock_canonical_diagnostic_carries_offending_duration() {
4822        // Diagnostic-shape pin: the canonical-form arm names the
4823        // offending `Duration` verbatim so the author's grep lands on
4824        // the field's value, not a generic "duration not canonical"
4825        // message. Same shape every other typed-cap arm on this
4826        // surface carries (`MemoryExceedsWasm32Cap` carries the
4827        // offending byte count verbatim, `PolicyRetriesExceedsCap`
4828        // carries the offending retry count verbatim,
4829        // `PolicyBreakerMaxFailuresExceedsCap` carries the offending
4830        // u32 verbatim).
4831        let w = Duration::from_micros(500);
4832        let l = LimitsSpec {
4833            wall_clock: Some(w),
4834            ..Default::default()
4835        };
4836        let err = l.validate().unwrap_err();
4837        let msg = err.to_string();
4838        assert!(
4839            msg.contains("500"),
4840            "diagnostic must carry the offending magnitude verbatim (got {msg:?})"
4841        );
4842    }
4843
4844    #[test]
4845    fn wall_clock_validated_value_round_trips_through_codec() {
4846        // The structural property the canonical-ms gate enforces:
4847        // every `LimitsSpec::wall_clock` past `LimitsSpec::validate`
4848        // round-trips losslessly through the in-module duration codec
4849        // (serialize → string → deserialize → equal value). Pin this
4850        // end-to-end so a future change to either side (the validate
4851        // gate's accepted granularity, the codec's parse/render unit
4852        // set) that breaks the alignment surfaces here. Peer of
4853        // `policy_timeout_validated_value_round_trips_through_codec` /
4854        // `circuit_breaker_window_validated_value_round_trips_through_codec`
4855        // on the sibling `:politicas` axes.
4856        for w in [
4857            Duration::from_millis(1),
4858            Duration::from_millis(1500),
4859            Duration::from_secs(30),
4860            Duration::from_secs(3600),
4861        ] {
4862            let l = LimitsSpec {
4863                wall_clock: Some(w),
4864                ..Default::default()
4865            };
4866            l.validate().unwrap();
4867            let json = serde_json::to_string(&l).unwrap();
4868            let back: LimitsSpec = serde_json::from_str(&json).unwrap();
4869            assert_eq!(
4870                back.wall_clock, l.wall_clock,
4871                "every validated :wall-clock must round-trip losslessly through the codec"
4872            );
4873        }
4874    }
4875
4876    // ── value-shape: :wall-clock upper bound — 1h ceiling ──────────────────
4877    //
4878    // The third typed-`Duration` axis brought to the uniform top edge
4879    // `LIMITS_WALL_CLOCK_MAX` = 1h established by the prior cap lifts
4880    // on `:politicas :timeout` (POLICY_TIMEOUT_MAX) and
4881    // `:politicas :circuit-breaker :window` (POLICY_BREAKER_WINDOW_MAX).
4882    // Mirrors the test discipline those peers carry: the
4883    // fail-before-pass-after pin, the 1ms-boundary pin, the
4884    // far-above-cap sweep (24h / 7d / ~11.5d — the values a
4885    // `(:wall-clock "24h")` typo or copy-paste typically lands), the
4886    // inclusive-at-cap positive control, the production-band positive-
4887    // control sweep, the cross-arm zero-then-cap and
4888    // canonical-then-cap ordering pins, the diagnostic-shape pin
4889    // carrying the offending `Duration` verbatim, and the cap-value
4890    // literal-identity + codec-round-trip pins anchoring the constant
4891    // to the codec's largest emitted unit and to its peer constants.
4892
4893    #[test]
4894    fn validate_rejects_wall_clock_above_cap() {
4895        // The fail-before-pass-after pin: 3601s = 1h + 1s is
4896        // structurally one canonical-tick past the
4897        // [`LIMITS_WALL_CLOCK_MAX`] ceiling (1h = 3600s) — an
4898        // integer-millisecond magnitude the canonical-form arm above
4899        // accepts cleanly, that the in-module duration codec
4900        // round-trips losslessly as `"3601s"`, and that silently
4901        // passed validate on every pre-gate codebase because the typed
4902        // slot's only checks were the zero-floor and canonical-form
4903        // arms. The wasm-engine consuming the value (the M2.5
4904        // `wasm-engine`'s epoch-deadline cancellation hook, the future
4905        // caixa-helm `pleme-computeunit` chart's `:limits` value
4906        // mapping) reaches for a `Duration` so long no realistic
4907        // synchronous wasm call hits it, far from the source
4908        // caixa.lisp.
4909        let w = LIMITS_WALL_CLOCK_MAX + Duration::from_secs(1);
4910        let l = LimitsSpec {
4911            wall_clock: Some(w),
4912            ..Default::default()
4913        };
4914        assert_eq!(
4915            l.validate().unwrap_err(),
4916            LimitsError::WallClockExceedsCap { wall_clock: w }
4917        );
4918    }
4919
4920    #[test]
4921    fn validate_rejects_wall_clock_one_millisecond_above_cap() {
4922        // Boundary case: exactly 1ms past the cap (the granularity the
4923        // canonical-form gate enforces). Catches a future "strictly
4924        // less than" half-measure and pins the diagnostic to name the
4925        // offending `Duration` verbatim. Peer of
4926        // `rejects_policy_timeout_one_millisecond_above_cap` /
4927        // `rejects_circuit_breaker_window_one_millisecond_above_cap`
4928        // on the sibling typed-`Duration` axes' top edges.
4929        let w = LIMITS_WALL_CLOCK_MAX + Duration::from_millis(1);
4930        let l = LimitsSpec {
4931            wall_clock: Some(w),
4932            ..Default::default()
4933        };
4934        assert_eq!(
4935            l.validate().unwrap_err(),
4936            LimitsError::WallClockExceedsCap { wall_clock: w }
4937        );
4938    }
4939
4940    #[test]
4941    fn validate_rejects_wall_clock_far_above_cap() {
4942        // The "obvious authoring footgun" case: a `(:wall-clock "24h")`
4943        // or `(:wall-clock "7d")` — values the canonical-form arm
4944        // accepts as integer-millisecond magnitudes, the codec
4945        // round-trips losslessly through serde, but the wasm-engine
4946        // cannot honor as a meaningful per-call deadline. Until this
4947        // gate landed validate accepted them. Pin the common
4948        // above-cap values (24h, 7d, ~11.5d) so a future relaxation
4949        // that drops the upper bound surfaces here.
4950        for w in [
4951            Duration::from_secs(86_400),    // 24h
4952            Duration::from_secs(604_800),   // 7d
4953            Duration::from_secs(1_000_000), // ~11.5 days
4954        ] {
4955            let l = LimitsSpec {
4956                wall_clock: Some(w),
4957                ..Default::default()
4958            };
4959            assert_eq!(
4960                l.validate().unwrap_err(),
4961                LimitsError::WallClockExceedsCap { wall_clock: w }
4962            );
4963        }
4964    }
4965
4966    #[test]
4967    fn validate_accepts_wall_clock_at_cap() {
4968        // The boundary value — exactly [`LIMITS_WALL_CLOCK_MAX`] (1h)
4969        // — must validate. The cap is inclusive on the top edge,
4970        // matching the [`crate::POLICY_TIMEOUT_MAX`] /
4971        // [`crate::POLICY_BREAKER_WINDOW_MAX`] /
4972        // [`LIMITS_MEMORY_WASM32_MAX_BYTES`] discipline on the sibling
4973        // capped axes. Pin the boundary explicitly so a future
4974        // off-by-one tightening (`>= LIMITS_WALL_CLOCK_MAX` instead of
4975        // `>`) surfaces here as a test failure rather than a silent
4976        // contract narrowing.
4977        let l = LimitsSpec {
4978            wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
4979            ..Default::default()
4980        };
4981        l.validate()
4982            .expect("wall_clock == LIMITS_WALL_CLOCK_MAX must validate");
4983    }
4984
4985    #[test]
4986    fn validate_accepts_wall_clock_typical_values() {
4987        // The documented per-request production-playbook band positive-
4988        // control sweep — every value Envoy / Istio / Linkerd / AWS
4989        // App Mesh / Kubernetes ingress-nginx recommend
4990        // (1ms..=3600s) must pass, plus a sweep through the
4991        // long-running-workflow band (5m, 15m, 30m, 1h) the cap
4992        // accepts. Mirrors `accepts_policy_timeout_typical_values` on
4993        // the sibling `:politicas :timeout` axis.
4994        for w in [
4995            Duration::from_millis(1),
4996            Duration::from_millis(500),
4997            Duration::from_secs(1),
4998            Duration::from_secs(10),
4999            Duration::from_secs(15), // Envoy default
5000            Duration::from_secs(30),
5001            Duration::from_secs(60),  // AWS App Mesh typical
5002            Duration::from_secs(300), // 5m
5003            Duration::from_secs(900), // 15m
5004            Duration::from_secs(1800),
5005            Duration::from_secs(3600), // exactly 1h, the cap
5006        ] {
5007            let l = LimitsSpec {
5008                wall_clock: Some(w),
5009                ..Default::default()
5010            };
5011            l.validate()
5012                .unwrap_or_else(|e| panic!("wall_clock={w:?} must validate; got {e:?}"));
5013        }
5014    }
5015
5016    #[test]
5017    fn wall_clock_zero_takes_precedence_over_cap() {
5018        // The cross-arm ordering pin: `Duration::ZERO` is structurally
5019        // outside both `>= 1ms` (zero-floor) and `<= LIMITS_WALL_CLOCK_MAX`
5020        // (cap), but the zero-floor diagnostic is the more
5021        // self-locating one (it directly names the omit-axis
5022        // remediation), so the validate gate must fire on zero first.
5023        // Same shape every other zero-then-shape ordering on this
5024        // surface uses (`MemoryZero` then `MemoryExceedsWasm32Cap`,
5025        // `PolicyTimeoutZero` then `PolicyTimeoutExceedsCap`).
5026        let l = LimitsSpec {
5027            wall_clock: Some(Duration::ZERO),
5028            ..Default::default()
5029        };
5030        assert_eq!(
5031            l.validate().unwrap_err(),
5032            LimitsError::WallClockZero,
5033            "Duration::ZERO must surface the zero-floor diagnostic, not the cap diagnostic"
5034        );
5035    }
5036
5037    #[test]
5038    fn wall_clock_canonical_takes_precedence_over_cap() {
5039        // The cross-arm ordering pin: a `Duration` that is *both*
5040        // sub-millisecond (non-canonical-form) and structurally above
5041        // the cap surfaces the canonical-form diagnostic first,
5042        // because the round-trip-shape break is the more fundamental
5043        // issue (the value can't even round-trip through the codec, so
5044        // the cap diagnostic naming `1ms..=1h` would be misleading —
5045        // there's no integer-ms form of the offending value). Pin the
5046        // order so a future refactor that reorders the arms surfaces
5047        // here as a test failure rather than a silent diagnostic
5048        // regression. Peer of
5049        // `policy_timeout_canonical_takes_precedence_over_cap`.
5050        let w = LIMITS_WALL_CLOCK_MAX + Duration::from_nanos(1);
5051        let l = LimitsSpec {
5052            wall_clock: Some(w),
5053            ..Default::default()
5054        };
5055        assert_eq!(
5056            l.validate().unwrap_err(),
5057            LimitsError::WallClockNotCanonical { wall_clock: w },
5058            "sub-ms above-cap value must surface the canonical-form diagnostic, not the cap diagnostic"
5059        );
5060    }
5061
5062    #[test]
5063    fn wall_clock_cap_diagnostic_carries_offending_value() {
5064        // The diagnostic-shape pin: the offending `Duration` is
5065        // carried verbatim into the
5066        // [`LimitsError::WallClockExceedsCap`] variant so the surfaced
5067        // error message names the value the author wrote, not just
5068        // the cap. Same self-locating diagnostic shape every other
5069        // typed-cap arm on this surface carries
5070        // (`MemoryExceedsWasm32Cap` carries the offending byte count
5071        // verbatim, `PolicyTimeoutExceedsCap` carries the offending
5072        // `Duration` verbatim).
5073        let w = Duration::from_secs(7200); // 2h
5074        let l = LimitsSpec {
5075            wall_clock: Some(w),
5076            ..Default::default()
5077        };
5078        let err = l.validate().unwrap_err();
5079        assert!(
5080            matches!(err, LimitsError::WallClockExceedsCap { wall_clock } if wall_clock == w),
5081            "got {err:?}"
5082        );
5083        let msg = err.to_string();
5084        assert!(
5085            msg.contains("7200"),
5086            ":limits :wall-clock cap diagnostic must carry the offending value verbatim (got: {msg})"
5087        );
5088    }
5089
5090    #[test]
5091    fn wall_clock_cap_pins_canonical_value() {
5092        // The [`LIMITS_WALL_CLOCK_MAX`] constant pins the value at
5093        // exactly 1 hour (3600s = 3_600_000ms) — the largest unit the
5094        // shared duration codec emits as a clean canonical string
5095        // (`"<n>h"`). Pinning the literal value here surfaces a future
5096        // drift (a relaxation to 24h, a tightening to 5m) as a
5097        // deliberate test edit, not a silent contract narrowing.
5098        //
5099        // The three typed-`Duration` caps on the validation surface
5100        // (`LIMITS_WALL_CLOCK_MAX` per-process, `POLICY_TIMEOUT_MAX`
5101        // per-edge, `POLICY_BREAKER_WINDOW_MAX` per-breaker) share a
5102        // single uniform top edge at the codec's largest emitted unit
5103        // — a structural-property invariant the equality assertions
5104        // here enshrine, so a future drift on any of the three
5105        // surfaces as a deliberate test edit. Same shape every other
5106        // typed-cap value pin uses
5107        // (`policy_timeout_cap_pins_canonical_value`,
5108        // `circuit_breaker_window_cap_pins_canonical_value`).
5109        assert_eq!(LIMITS_WALL_CLOCK_MAX, Duration::from_secs(3600));
5110        assert_eq!(LIMITS_WALL_CLOCK_MAX.as_millis(), 3_600_000);
5111        assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_TIMEOUT_MAX);
5112        assert_eq!(LIMITS_WALL_CLOCK_MAX, crate::POLICY_BREAKER_WINDOW_MAX);
5113    }
5114
5115    #[test]
5116    fn wall_clock_cap_value_round_trips_through_codec() {
5117        // The codec round-trip property the cap arm preserves: the
5118        // [`LIMITS_WALL_CLOCK_MAX`] constant itself round-trips through
5119        // the in-module duration codec — every value at the cap
5120        // renders to a clean canonical string (`"1h"`) and parses back
5121        // to the same `Duration`. Pin this so a future drift between
5122        // the cap constant and the codec's largest emitted unit
5123        // surfaces here. Same shape every other typed boundary pin on
5124        // this surface uses
5125        // (`wasm32_memory_cap_matches_parsed_4_gib`,
5126        // `policy_timeout_cap_value_round_trips_through_codec`).
5127        let l = LimitsSpec {
5128            wall_clock: Some(LIMITS_WALL_CLOCK_MAX),
5129            ..Default::default()
5130        };
5131        let json = serde_json::to_string(&l).unwrap();
5132        assert!(
5133            json.contains("\"1h\""),
5134            "the LIMITS_WALL_CLOCK_MAX value must render to the canonical \"1h\" form (got: {json})"
5135        );
5136        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5137        assert_eq!(back.wall_clock, Some(LIMITS_WALL_CLOCK_MAX));
5138        l.validate()
5139            .expect("LIMITS_WALL_CLOCK_MAX itself must pass validate");
5140    }
5141
5142    // ── value-shape: :cpu upper bound — 128-core schedulability ceiling ─────
5143    //
5144    // The third `LimitsSpec` axis brought to a top-edge cap, peer to
5145    // the `:memory` wasm32 ceiling and the `:wall-clock` 1h ceiling.
5146    // Mirrors the test discipline those peers carry: the
5147    // fail-before-pass-after pin, the one-millicore-boundary pin, the
5148    // far-above-cap sweep, the inclusive-at-cap positive control, the
5149    // production-band positive-control sweep, the cross-arm zero-then-
5150    // cap ordering pin, the diagnostic-shape pin carrying the offending
5151    // value verbatim, and the cap-value literal-identity + codec
5152    // round-trip pins anchoring the constant.
5153
5154    #[test]
5155    fn validate_rejects_cpu_above_cap() {
5156        // The fail-before-pass-after pin: 128_001m = 128 cores + 1
5157        // millicore is structurally one canonical-tick past the
5158        // [`LIMITS_CPU_MILLICORES_MAX`] ceiling — a `u32` magnitude the
5159        // millicore codec round-trips losslessly as `"128001m"`, and
5160        // that silently passed validate on every pre-gate codebase
5161        // because the typed slot's only check was the zero-floor arm.
5162        // The Kubernetes scheduler consuming the value (via the
5163        // `pleme-computeunit` chart's `resources.requests.cpu`
5164        // projection) cannot bind the pod to any node, far from the
5165        // source caixa.lisp.
5166        let m = LIMITS_CPU_MILLICORES_MAX + 1;
5167        let l = LimitsSpec {
5168            cpu: Some(m),
5169            ..Default::default()
5170        };
5171        assert_eq!(
5172            l.validate().unwrap_err(),
5173            LimitsError::CpuExceedsCap { millicores: m }
5174        );
5175    }
5176
5177    #[test]
5178    fn validate_rejects_cpu_far_above_cap() {
5179        // The "obvious authoring footgun" case: a `(:cpu "1000000m")`
5180        // (1000 cores) or `(:cpu "4294967295m")` (≈ u32::MAX) — values
5181        // the millicore codec accepts cleanly, the codec round-trips
5182        // losslessly through serde, but the Kubernetes scheduler
5183        // cannot bind to any node. Until this gate landed validate
5184        // accepted them. Pin the common above-cap values (1000 cores,
5185        // 10_000 cores, u32::MAX) so a future relaxation that drops
5186        // the upper bound surfaces here. Peer of
5187        // `validate_rejects_memory_8_gib` /
5188        // `validate_rejects_wall_clock_far_above_cap`.
5189        for m in [1_000_000_u32, 10_000_000, u32::MAX] {
5190            let l = LimitsSpec {
5191                cpu: Some(m),
5192                ..Default::default()
5193            };
5194            assert_eq!(
5195                l.validate().unwrap_err(),
5196                LimitsError::CpuExceedsCap { millicores: m }
5197            );
5198        }
5199    }
5200
5201    #[test]
5202    fn validate_accepts_cpu_at_cap() {
5203        // The boundary value — exactly [`LIMITS_CPU_MILLICORES_MAX`]
5204        // (128 cores = 128_000m) — must validate. The cap is inclusive
5205        // on the top edge, matching the discipline on every sibling
5206        // capped axis ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
5207        // [`LIMITS_WALL_CLOCK_MAX`], [`crate::POLICY_TIMEOUT_MAX`],
5208        // [`crate::POLICY_BREAKER_WINDOW_MAX`],
5209        // [`crate::POLICY_RATE_LIMIT_MAX`]). Pin the boundary
5210        // explicitly so a future off-by-one tightening
5211        // (`>= LIMITS_CPU_MILLICORES_MAX` instead of `>`) surfaces here
5212        // as a test failure rather than a silent contract narrowing.
5213        let l = LimitsSpec {
5214            cpu: Some(LIMITS_CPU_MILLICORES_MAX),
5215            ..Default::default()
5216        };
5217        l.validate()
5218            .expect("cpu == LIMITS_CPU_MILLICORES_MAX must validate");
5219    }
5220
5221    #[test]
5222    fn validate_accepts_cpu_typical_values() {
5223        // The documented production-playbook band positive-control
5224        // sweep — every value the canonical caixa Servico runs in
5225        // (100m..=2000m) must pass, plus a sweep through the larger
5226        // burstable / multi-component-host band (4000m, 8000m, 16000m,
5227        // 32000m, 64000m, 128000m) the cap accepts. Mirrors
5228        // `accepts_wall_clock_typical_values` on the sibling
5229        // `:wall-clock` axis.
5230        for m in [
5231            1_u32,   // smallest non-zero
5232            100,     // typical small worker
5233            500,     // canonical test default (peer to limits/flux/helm)
5234            1_000,   // 1 core, single-threaded wasm32 saturation
5235            2_000,   // 2 cores
5236            4_000,   // typical burstable
5237            8_000,   // upper realistic per-Servico band
5238            16_000,  // documented heavy-Servico ceiling
5239            32_000,  // wide-node multi-component-host
5240            64_000,  // half the cap
5241            128_000, // exactly at cap
5242        ] {
5243            let l = LimitsSpec {
5244                cpu: Some(m),
5245                ..Default::default()
5246            };
5247            l.validate()
5248                .unwrap_or_else(|e| panic!("cpu={m}m must validate; got {e:?}"));
5249        }
5250    }
5251
5252    #[test]
5253    fn cpu_zero_takes_precedence_over_cap() {
5254        // The cross-arm ordering pin: `Some(0)` is structurally outside
5255        // both `>= 1` (zero-floor) and `<= LIMITS_CPU_MILLICORES_MAX`
5256        // (cap), but the zero-floor diagnostic is the more
5257        // self-locating one (it directly names the omit-axis
5258        // remediation), so the validate gate must fire on zero first.
5259        // Same shape every other zero-then-cap ordering on this surface
5260        // uses (`MemoryZero` then `MemoryExceedsWasm32Cap`,
5261        // `WallClockZero` then `WallClockExceedsCap`).
5262        let l = LimitsSpec {
5263            cpu: Some(0),
5264            ..Default::default()
5265        };
5266        assert_eq!(
5267            l.validate().unwrap_err(),
5268            LimitsError::CpuZero,
5269            "Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
5270        );
5271    }
5272
5273    #[test]
5274    fn validate_rejects_cpu_cap_after_earlier_axes() {
5275        // Cross-axis ordering: when both an above-cap `:cpu` and an
5276        // earlier-axis violation are present, the earlier axis must
5277        // fire first. The validate sequence is :memory → :fuel →
5278        // :wall-clock → :cpu, so a paired memory-zero + cpu-above-cap
5279        // input surfaces `MemoryZero`, never the cpu-cap diagnostic.
5280        // Pins the canonical axis order so a future refactor that
5281        // reorders the arms surfaces here as a test failure rather
5282        // than a silent diagnostic regression. Peer of
5283        // `validate_rejects_first_zero_axis_deterministically` and
5284        // `validate_rejects_memory_cap_before_other_axes`.
5285        let l = LimitsSpec {
5286            memory: Some(0),
5287            fuel: None,
5288            wall_clock: None,
5289            cpu: Some(LIMITS_CPU_MILLICORES_MAX + 1),
5290        };
5291        assert_eq!(
5292            l.validate().unwrap_err(),
5293            LimitsError::MemoryZero,
5294            "earlier-axis violation must take precedence over later-axis cap violation"
5295        );
5296    }
5297
5298    #[test]
5299    fn cpu_cap_diagnostic_carries_offending_value() {
5300        // The diagnostic-shape pin: the offending millicore count is
5301        // carried verbatim into the [`LimitsError::CpuExceedsCap`]
5302        // variant so the surfaced error message names the value the
5303        // author wrote, not just the cap. Same self-locating
5304        // diagnostic shape every other typed-cap arm on this surface
5305        // carries (`MemoryExceedsWasm32Cap` carries the offending byte
5306        // count verbatim, `WallClockExceedsCap` carries the offending
5307        // `Duration` verbatim).
5308        let m = 256_000_u32; // 256 cores — double the cap
5309        let l = LimitsSpec {
5310            cpu: Some(m),
5311            ..Default::default()
5312        };
5313        let err = l.validate().unwrap_err();
5314        assert!(
5315            matches!(err, LimitsError::CpuExceedsCap { millicores } if millicores == m),
5316            "got {err:?}"
5317        );
5318        let msg = err.to_string();
5319        assert!(
5320            msg.contains("256000"),
5321            ":limits :cpu cap diagnostic must carry the offending value verbatim (got: {msg})"
5322        );
5323    }
5324
5325    #[test]
5326    fn cpu_cap_pins_canonical_value() {
5327        // The [`LIMITS_CPU_MILLICORES_MAX`] constant pins the value at
5328        // exactly 128 cores (128_000 millicores) — the largest
5329        // commercially-common non-metal cloud Kubernetes node vCPU
5330        // count. Pinning the literal value here surfaces a future
5331        // drift (a relaxation to 256 cores, a tightening to 64 cores)
5332        // as a deliberate test edit, not a silent contract narrowing.
5333        // Same shape every other typed-cap value pin uses
5334        // (`wall_clock_cap_pins_canonical_value`,
5335        // `wasm32_memory_cap_matches_parsed_4_gib`).
5336        assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128_000);
5337        assert_eq!(LIMITS_CPU_MILLICORES_MAX, 128 * 1000);
5338    }
5339
5340    #[test]
5341    fn cpu_cap_value_round_trips_through_codec() {
5342        // The codec round-trip property the cap arm preserves: the
5343        // [`LIMITS_CPU_MILLICORES_MAX`] constant itself round-trips
5344        // through the in-module millicore codec — the cap value
5345        // renders to a clean canonical string (`"128000m"`) and parses
5346        // back to the same `u32`. Pin this so a future drift between
5347        // the cap constant and the codec's accepted magnitude surfaces
5348        // here. Same shape every other typed boundary pin on this
5349        // surface uses (`wasm32_memory_cap_matches_parsed_4_gib`,
5350        // `wall_clock_cap_value_round_trips_through_codec`).
5351        let l = LimitsSpec {
5352            cpu: Some(LIMITS_CPU_MILLICORES_MAX),
5353            ..Default::default()
5354        };
5355        let json = serde_json::to_string(&l).unwrap();
5356        assert!(
5357            json.contains("\"128000m\""),
5358            "the LIMITS_CPU_MILLICORES_MAX value must render to the canonical \"128000m\" form (got: {json})"
5359        );
5360        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5361        assert_eq!(back.cpu, Some(LIMITS_CPU_MILLICORES_MAX));
5362        l.validate()
5363            .expect("LIMITS_CPU_MILLICORES_MAX itself must pass validate");
5364    }
5365
5366    // ── value-shape: :fuel upper bound — 10^12 no-op-budget ceiling ────────
5367    //
5368    // The fourth and final `LimitsSpec` axis brought to a top-edge
5369    // cap, closing the open edge the 857dfcc CPU-cap commit body
5370    // explicitly named: "three of the four axes carry a top-and-bottom
5371    // edge gate; only `:fuel` remains with a zero-floor-only shape."
5372    // Mirrors the test discipline every sibling capped axis carries:
5373    // the fail-before-pass-after pin, the one-instruction-boundary
5374    // pin, the far-above-cap sweep, the inclusive-at-cap positive
5375    // control, the production-band positive-control sweep, the
5376    // cross-arm zero-then-cap ordering pin, the cross-axis
5377    // earlier-then-later precedence pin, the diagnostic-shape pin
5378    // carrying the offending value verbatim, and the cap-value
5379    // literal-identity + codec round-trip pins anchoring the
5380    // constant.
5381
5382    #[test]
5383    fn validate_rejects_fuel_above_cap() {
5384        // The fail-before-pass-after pin: `LIMITS_FUEL_MAX + 1` =
5385        // one wasm-instruction past the structural ceiling — a `u64`
5386        // magnitude the typed slot round-trips losslessly through
5387        // serde, and that silently passed validate on every pre-gate
5388        // codebase because the typed slot's only check was the
5389        // zero-floor arm. The wasm-engine consuming the value (via
5390        // `Store::set_fuel` projection in the M2.5 host runtime)
5391        // accepts the magnitude but the sibling `:wall-clock` 1h cap
5392        // fires before the fuel counter could ever drain — the typed
5393        // `:fuel` slot becomes a no-op budget far from the source
5394        // caixa.lisp.
5395        let f = LIMITS_FUEL_MAX + 1;
5396        let l = LimitsSpec {
5397            fuel: Some(f),
5398            ..Default::default()
5399        };
5400        assert_eq!(
5401            l.validate().unwrap_err(),
5402            LimitsError::FuelExceedsCap { fuel: f }
5403        );
5404    }
5405
5406    #[test]
5407    fn validate_rejects_fuel_far_above_cap() {
5408        // The "obvious authoring footgun" case: a `(:fuel
5409        // 1000000000000000)` (10^15 instructions), a paste-from-binary
5410        // `u64::MAX`, or a hex-literal-confused-for-decimal magnitude
5411        // — values the `u64` slot accepts cleanly, the codec
5412        // round-trips losslessly through serde, but the wasm-engine
5413        // can never honor as a meaningful counter. Until this gate
5414        // landed validate accepted them. Pin the common above-cap
5415        // values (10x cap, 1000x cap, `u64::MAX`) so a future
5416        // relaxation that drops the upper bound surfaces here. Peer
5417        // of `validate_rejects_cpu_far_above_cap` /
5418        // `validate_rejects_memory_8_gib` /
5419        // `validate_rejects_wall_clock_far_above_cap`.
5420        for f in [LIMITS_FUEL_MAX * 10, LIMITS_FUEL_MAX * 1_000, u64::MAX] {
5421            let l = LimitsSpec {
5422                fuel: Some(f),
5423                ..Default::default()
5424            };
5425            assert_eq!(
5426                l.validate().unwrap_err(),
5427                LimitsError::FuelExceedsCap { fuel: f }
5428            );
5429        }
5430    }
5431
5432    #[test]
5433    fn validate_accepts_fuel_at_cap() {
5434        // The boundary value — exactly [`LIMITS_FUEL_MAX`] (10^12
5435        // wasm instructions) — must validate. The cap is inclusive
5436        // on the top edge, matching the discipline on every sibling
5437        // capped axis ([`LIMITS_MEMORY_WASM32_MAX_BYTES`],
5438        // [`LIMITS_WALL_CLOCK_MAX`], [`LIMITS_CPU_MILLICORES_MAX`],
5439        // [`crate::POLICY_TIMEOUT_MAX`],
5440        // [`crate::POLICY_BREAKER_WINDOW_MAX`],
5441        // [`crate::POLICY_RATE_LIMIT_MAX`]). Pin the boundary
5442        // explicitly so a future off-by-one tightening
5443        // (`>= LIMITS_FUEL_MAX` instead of `>`) surfaces here as a
5444        // test failure rather than a silent contract narrowing.
5445        let l = LimitsSpec {
5446            fuel: Some(LIMITS_FUEL_MAX),
5447            ..Default::default()
5448        };
5449        l.validate().expect("fuel == LIMITS_FUEL_MAX must validate");
5450    }
5451
5452    #[test]
5453    fn validate_accepts_fuel_typical_values() {
5454        // The documented production-playbook band positive-control
5455        // sweep — every value the canonical caixa Servico runs in
5456        // (10^6..=10^9 fuel-units) must pass, plus a sweep through
5457        // the larger compute-bound-Servico band (10^10, 10^11) the
5458        // cap accepts. The canonical fixture is `1_000_000` =
5459        // wasmtime's documented `Store::set_fuel(1_000_000)` example.
5460        // Mirrors `validate_accepts_cpu_typical_values` on the
5461        // sibling `:cpu` axis.
5462        for f in [
5463            1_u64,             // smallest non-zero
5464            1_000,             // tiny per-call budget
5465            1_000_000,         // canonical fixture (10^6) — wasmtime book example
5466            10_000_000,        // typical small-Servico (10^7)
5467            100_000_000,       // typical heavier-Servico (10^8)
5468            1_000_000_000,     // 1 billion — upper realistic per-call (10^9)
5469            100_000_000_000,   // 10^11 — heavy compute-bound (10x below cap)
5470            500_000_000_000,   // half the cap
5471            1_000_000_000_000, // exactly at cap (10^12)
5472        ] {
5473            let l = LimitsSpec {
5474                fuel: Some(f),
5475                ..Default::default()
5476            };
5477            l.validate()
5478                .unwrap_or_else(|e| panic!("fuel={f} must validate; got {e:?}"));
5479        }
5480    }
5481
5482    #[test]
5483    fn fuel_zero_takes_precedence_over_cap() {
5484        // The cross-arm ordering pin: `Some(0)` is structurally
5485        // outside both `>= 1` (zero-floor) and `<= LIMITS_FUEL_MAX`
5486        // (cap), but the zero-floor diagnostic is the more
5487        // self-locating one (it directly names the omit-axis
5488        // remediation and the wasmtime-traps-at-zero semantics), so
5489        // the validate gate must fire on zero first. Same shape every
5490        // other zero-then-cap ordering on this surface uses
5491        // (`MemoryZero` then `MemoryExceedsWasm32Cap`,
5492        // `WallClockZero` then `WallClockExceedsCap`, `CpuZero` then
5493        // `CpuExceedsCap`).
5494        let l = LimitsSpec {
5495            fuel: Some(0),
5496            ..Default::default()
5497        };
5498        assert_eq!(
5499            l.validate().unwrap_err(),
5500            LimitsError::FuelZero,
5501            "Some(0) must surface the zero-floor diagnostic, not the cap diagnostic"
5502        );
5503    }
5504
5505    #[test]
5506    fn validate_rejects_fuel_cap_after_earlier_axes() {
5507        // Cross-axis ordering: when both an above-cap `:fuel` and an
5508        // earlier-axis violation are present, the earlier axis must
5509        // fire first. The validate sequence is :memory → :fuel →
5510        // :wall-clock → :cpu, so a paired memory-zero + fuel-above-
5511        // cap input surfaces `MemoryZero`, never the fuel-cap
5512        // diagnostic. Pins the canonical axis order so a future
5513        // refactor that reorders the arms surfaces here as a test
5514        // failure rather than a silent diagnostic regression. Peer
5515        // of `validate_rejects_cpu_cap_after_earlier_axes`.
5516        let l = LimitsSpec {
5517            memory: Some(0),
5518            fuel: Some(LIMITS_FUEL_MAX + 1),
5519            wall_clock: None,
5520            cpu: None,
5521        };
5522        assert_eq!(
5523            l.validate().unwrap_err(),
5524            LimitsError::MemoryZero,
5525            "earlier-axis violation must take precedence over later-axis cap violation"
5526        );
5527    }
5528
5529    #[test]
5530    fn validate_rejects_fuel_cap_before_later_axes() {
5531        // Cross-axis ordering on the other side: when both an
5532        // above-cap `:fuel` and a later-axis violation are present,
5533        // the `:fuel` cap must fire before the `:wall-clock` /
5534        // `:cpu` zero-floor diagnostics. The validate sequence is
5535        // :memory → :fuel → :wall-clock → :cpu, so a paired
5536        // fuel-above-cap + wall-clock-zero input surfaces
5537        // `FuelExceedsCap`, not `WallClockZero`. Pins the canonical
5538        // axis order on the new arm's downstream side, peer to the
5539        // upstream pin `validate_rejects_fuel_cap_after_earlier_axes`.
5540        let l = LimitsSpec {
5541            memory: None,
5542            fuel: Some(LIMITS_FUEL_MAX + 1),
5543            wall_clock: Some(Duration::ZERO),
5544            cpu: Some(0),
5545        };
5546        assert_eq!(
5547            l.validate().unwrap_err(),
5548            LimitsError::FuelExceedsCap {
5549                fuel: LIMITS_FUEL_MAX + 1
5550            },
5551            ":fuel cap diagnostic must take precedence over later-axis zero-floor diagnostics"
5552        );
5553    }
5554
5555    #[test]
5556    fn fuel_cap_diagnostic_carries_offending_value() {
5557        // The diagnostic-shape pin: the offending fuel count is
5558        // carried verbatim into the [`LimitsError::FuelExceedsCap`]
5559        // variant so the surfaced error message names the value the
5560        // author wrote, not just the cap. Same self-locating
5561        // diagnostic shape every other typed-cap arm on this surface
5562        // carries (`MemoryExceedsWasm32Cap` carries the offending
5563        // byte count verbatim, `WallClockExceedsCap` carries the
5564        // offending `Duration` verbatim, `CpuExceedsCap` carries the
5565        // offending millicore count verbatim).
5566        let f = 5_000_000_000_000_u64; // 5 trillion — 5x the cap
5567        let l = LimitsSpec {
5568            fuel: Some(f),
5569            ..Default::default()
5570        };
5571        let err = l.validate().unwrap_err();
5572        assert!(
5573            matches!(err, LimitsError::FuelExceedsCap { fuel } if fuel == f),
5574            "got {err:?}"
5575        );
5576        let msg = err.to_string();
5577        assert!(
5578            msg.contains("5000000000000"),
5579            ":limits :fuel cap diagnostic must carry the offending value verbatim (got: {msg})"
5580        );
5581    }
5582
5583    #[test]
5584    fn fuel_cap_pins_canonical_value() {
5585        // The [`LIMITS_FUEL_MAX`] constant pins the value at exactly
5586        // 10^12 (1 trillion wasm instructions) — the round-number
5587        // ceiling above the operational envelope the sibling
5588        // [`LIMITS_WALL_CLOCK_MAX`] (1h) × wasmtime's fuel-tracked
5589        // execution rate (~10^9 fuel/sec) yields. Pinning the
5590        // literal value here surfaces a future drift (a relaxation
5591        // to 10^15, a tightening to 10^9) as a deliberate test edit,
5592        // not a silent contract narrowing. Same shape every other
5593        // typed-cap value pin uses (`cpu_cap_pins_canonical_value`,
5594        // `wall_clock_cap_pins_canonical_value`,
5595        // `wasm32_memory_cap_matches_parsed_4_gib`).
5596        assert_eq!(LIMITS_FUEL_MAX, 1_000_000_000_000);
5597        assert_eq!(LIMITS_FUEL_MAX, 10_u64.pow(12));
5598    }
5599
5600    #[test]
5601    fn fuel_cap_value_round_trips_through_serde() {
5602        // The serde round-trip property the cap arm preserves: the
5603        // [`LIMITS_FUEL_MAX`] constant itself round-trips through
5604        // the in-module `u64` serde codec — the cap value renders as
5605        // the bare integer literal and parses back to the same
5606        // `u64`. Pin this so a future drift between the cap constant
5607        // and the codec's accepted magnitude (a future custom u64
5608        // serializer that introduces lossy formatting) surfaces
5609        // here. Same shape every other typed boundary pin on this
5610        // surface uses (`wasm32_memory_cap_matches_parsed_4_gib`,
5611        // `wall_clock_cap_value_round_trips_through_codec`,
5612        // `cpu_cap_value_round_trips_through_codec`).
5613        let l = LimitsSpec {
5614            fuel: Some(LIMITS_FUEL_MAX),
5615            ..Default::default()
5616        };
5617        let json = serde_json::to_string(&l).unwrap();
5618        assert!(
5619            json.contains("1000000000000"),
5620            "the LIMITS_FUEL_MAX value must render verbatim as the bare integer 10^12 \
5621             (got: {json})"
5622        );
5623        let back: LimitsSpec = serde_json::from_str(&json).unwrap();
5624        assert_eq!(back.fuel, Some(LIMITS_FUEL_MAX));
5625        l.validate()
5626            .expect("LIMITS_FUEL_MAX itself must pass validate");
5627    }
5628
5629    // ── per-`:limits :memory` accessor pins (LimitsSpec::memory) ─────────
5630
5631    #[test]
5632    fn limits_memory_returns_option_u64_byte_equal_across_permutations() {
5633        // The canonical per-`:limits` `:memory` Lunatic-per-process
5634        // wasm32-linear-memory byte-cap scalar pin: [`LimitsSpec::memory`]
5635        // must return the `:limits :memory` typed `u64` verbatim as an
5636        // `Option<u64>`, byte-equal to the raw field access across the
5637        // three canonical shape-arms — `None` (no cap declared —
5638        // engine-default applies), `Some(LIMITS_MEMORY_WASM32_PAGE_BYTES)`
5639        // (the structural minimum a validated `:limits :memory` may
5640        // carry, one wasm32 linear-memory page), `Some(64 * 1024 *
5641        // 1024)` (the canonical 64 MiB byte-cap the module-level
5642        // docstring names).
5643        //
5644        // Peer of the sibling per-`:politicas` [`crate::MeshPolicy::mtls_required`]
5645        // (c0110f1) / [`crate::MeshPolicy::retries`] (bdfb399) /
5646        // [`crate::MeshPolicy::timeout`] (7073d0f) accessor pin trio on
5647        // the sibling `Option<Copy-T>`-return axis, extended to the
5648        // peer per-`:limits` typed-`u64` optional-scalar shape —
5649        // first `Option<Copy-T>`-return accessor on the M2 slot family.
5650        // Pins against a future silent detour that re-derived the cap
5651        // from a peer axis (an accidental `.fuel`-collapse that
5652        // assumed the two `Option<u64>` axes carry the same value), a
5653        // `None` → `Some(0)` "zero means unbounded" collapse (the
5654        // canonical `Option<u64>` → `u64` collapse footgun the
5655        // [`LimitsError::MemoryZero`] validate arm guards on the peer
5656        // zero-floor axis), or a per-arm variant swap that landed on
5657        // one consumer without the other.
5658        for memory in [
5659            None,
5660            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
5661            Some(64 * 1024 * 1024),
5662        ] {
5663            let l = LimitsSpec {
5664                memory,
5665                ..LimitsSpec::default()
5666            };
5667            assert_eq!(
5668                l.memory(),
5669                memory,
5670                "LimitsSpec::memory must return :limits :memory verbatim \
5671                 (got {:?}, expected {memory:?})",
5672                l.memory(),
5673            );
5674            assert_eq!(
5675                l.memory(),
5676                l.memory,
5677                "LimitsSpec::memory must byte-equal the raw .memory \
5678                 field access across every value in the accept-set",
5679            );
5680        }
5681    }
5682
5683    #[test]
5684    fn limits_is_empty_memory_arm_routes_through_accessor() {
5685        // Composition pin: [`LimitsSpec::is_empty`]'s `memory` arm
5686        // must key off [`LimitsSpec::memory`], not the raw `.memory`
5687        // field access. Structurally: setting ONLY the `memory` slot
5688        // on an otherwise-default LimitsSpec must flip `is_empty()`
5689        // from `true` (all-`None`) to `false` (one axis carries a
5690        // value); the flip must be observed across every value in the
5691        // accept-set since the emptiness semantic reads "any axis
5692        // carries a value" — not "any axis carries a value above a
5693        // threshold" — the same non-collapsing shape the sibling M3
5694        // [`crate::MeshPolicy::is_empty`] predicate carries on its
5695        // peer `Option<Copy-T>`-typed slot surfaces.
5696        //
5697        // Pins against a future silent detour that re-derived the
5698        // emptiness predicate off a peer axis (an accidental
5699        // `.fuel.is_none()`-only chain that dropped the `memory` arm
5700        // entirely), an accessor-side detour that no longer names the
5701        // substrate-primitive typed dispatch (an accidental
5702        // `self.memory.unwrap_or(0) == 0` fallback in the accessor
5703        // that would silently classify both `None` and `Some(0)` as
5704        // the same value), or a threshold collapse (a
5705        // `self.memory().is_some_and(|m| m > 0)` that would silently
5706        // classify `Some(0)` as unset).
5707        //
5708        // Peer of the sibling per-`:politicas`
5709        // [`crate::MeshPolicy::is_empty`] `mtls_required` arm
5710        // accessor-composition pin (c0110f1) on the sibling optional-
5711        // scalar axis — same "the emptiness / shape-gate predicate
5712        // must route through the substrate-primitive typed dispatch"
5713        // discipline extended onto the peer per-`:limits` emptiness
5714        // predicate.
5715        let empty = LimitsSpec::default();
5716        assert!(
5717            empty.is_empty(),
5718            "LimitsSpec::default() must be is_empty() — every axis \
5719             defaults to None",
5720        );
5721        for memory in [
5722            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
5723            Some(64 * 1024 * 1024),
5724            Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
5725        ] {
5726            let l = LimitsSpec {
5727                memory,
5728                ..LimitsSpec::default()
5729            };
5730            assert!(
5731                !l.is_empty(),
5732                "LimitsSpec::is_empty must return false when :memory \
5733                 is {memory:?} — the emptiness predicate reads \"any \
5734                 axis carries a value\", not \"any axis carries a \
5735                 value above a threshold\"",
5736            );
5737            assert_eq!(
5738                l.memory().is_none(),
5739                l.is_empty(),
5740                "when :memory is the only set axis, is_empty() must \
5741                 equal memory().is_none() — the accessor and the \
5742                 emptiness predicate must route through the same \
5743                 substrate-primitive typed dispatch on the :memory \
5744                 arm",
5745            );
5746        }
5747    }
5748
5749    #[test]
5750    fn limits_memory_projects_option_u64_by_copy() {
5751        // The by-copy pin: [`LimitsSpec::memory`] returns `Option<u64>`
5752        // by copy — `Option<u64>` is `Copy` and the accessor must
5753        // return by value, not by reference. Peer of the sibling per-
5754        // `:politicas` [`crate::MeshPolicy::mtls_required`] (c0110f1)
5755        // borrow-invariant pin on the peer `Option<bool>` shape,
5756        // extended onto the peer `Option<u64>` copy-invariant shape —
5757        // the accessor's returned `Option<u64>` must outlive `&self`
5758        // (multiple calls must return equal values from a dropped-
5759        // `&self` copy, since the returned Option carries no borrow),
5760        // and calling the accessor twice on the same LimitsSpec must
5761        // yield the same `Option<u64>` verbatim (idempotent, no side
5762        // effects on `&self`).
5763        //
5764        // Pins against a future silent detour that returned
5765        // `Option<&u64>` (which would type-check but silently break
5766        // every downstream caller — the future `wasmtime::Store::limiter`
5767        // wire path consumes `Option<u64>` by value and `&u64` would
5768        // fold to a detached copy at the call site), an accidental
5769        // `Option::as_ref()` projection (`self.memory.as_ref()` would
5770        // also type-check but return `Option<&u64>`), or a one-arm-
5771        // only accessor that reads `Some(*m)` in the Some arm but
5772        // reads a fresh `Default::default()` in the None arm.
5773        for memory in [
5774            None,
5775            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
5776            Some(64 * 1024 * 1024),
5777            Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
5778        ] {
5779            let l = LimitsSpec {
5780                memory,
5781                ..LimitsSpec::default()
5782            };
5783            let first = l.memory();
5784            let second = l.memory();
5785            assert_eq!(
5786                first, second,
5787                "LimitsSpec::memory must be idempotent — two \
5788                 successive calls on the same &self must return the \
5789                 same Option<u64>",
5790            );
5791            assert_eq!(
5792                first, memory,
5793                "LimitsSpec::memory must return :limits :memory \
5794                 verbatim by copy — got {first:?}, expected {memory:?}",
5795            );
5796        }
5797    }
5798
5799    #[test]
5800    #[allow(clippy::too_many_lines)]
5801    fn validate_memory_arms_route_through_lifted_memory_accessor() {
5802        // Composition pin: every value-shape gate in
5803        // [`LimitsSpec::validate`] on the `:memory` axis (the
5804        // zero-floor `MemoryZero` arm, the sub-page `MemoryBelowWasm32Page`
5805        // arm, the above-cap `MemoryExceedsWasm32Cap` arm, the
5806        // non-page-multiple `MemoryNotPageMultiple` arm) must key off
5807        // [`LimitsSpec::memory`], not the raw `self.memory` field
5808        // access. Peer of the sibling per-`:politicas`
5809        // [`crate::AplicacaoSpec::validate_politicas`] `:timeout` /
5810        // `:retries` arm converge pin (1017b9d) on the sibling M3
5811        // mesh-slot family, extended onto the M2 per-`:limits`
5812        // `:memory` axis; peer of the sibling per-`:limits` `:fuel` /
5813        // `:wall-clock` / `:cpu` arms in the same fan-out that
5814        // already route through `self.fuel()` / `self.wall_clock()`
5815        // / `self.cpu()` at :880 / :888 / :942.
5816        //
5817        // Assertion shape: for each memory value in the
5818        // accept-and-refuse set, `LimitsSpec::memory()` must byte-
5819        // equal the raw `.memory` field it borrows from, and the
5820        // validate call on a `LimitsSpec { memory: <v>, ..default() }`
5821        // fixture must surface the same variant/Ok discriminant the
5822        // accessor-composed spec surfaces. Together they catch any
5823        // future silent detour — an accessor drift that no longer
5824        // shipped the raw slot verbatim, a validate-branch rebrand to
5825        // a peer-axis field read, an accidental `Option`-collapse in
5826        // any of the four arms — at caixa-core build time rather than
5827        // at a downstream runtime declared-but-inert-limits divergence
5828        // at the wasmtime `Store::limiter` boundary.
5829        //
5830        // `#[allow(clippy::too_many_lines)]` per the same discipline
5831        // peer over-100-line composition pins in this module accept
5832        // (see e.g. `limits_is_empty_memory_arm_routes_through_accessor`,
5833        // `limits_memory_returns_option_u64_byte_equal_across_permutations`).
5834        for memory in [
5835            None,
5836            Some(0),                                   // → MemoryZero
5837            Some(1),                                   // → MemoryBelowWasm32Page (sub-page)
5838            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES - 1), // → MemoryBelowWasm32Page (at-under-page)
5839            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),     // → Ok (at-page-floor)
5840            Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1), // → MemoryNotPageMultiple (one-past-page)
5841            Some(2 * LIMITS_MEMORY_WASM32_PAGE_BYTES), // → Ok (multi-page)
5842            Some(LIMITS_MEMORY_WASM32_MAX_BYTES),      // → Ok (at-cap)
5843            Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),  // → MemoryExceedsWasm32Cap (one-past-cap)
5844        ] {
5845            let l = LimitsSpec {
5846                memory,
5847                ..LimitsSpec::default()
5848            };
5849            // (1) The accessor must byte-equal the raw field it wraps.
5850            assert_eq!(
5851                l.memory(),
5852                l.memory,
5853                "LimitsSpec::memory() must byte-equal the raw \
5854                 .memory field for {memory:?} — an accessor detour \
5855                 that dropped the raw slot's Option<u64> verbatim \
5856                 would silently split validate's :memory arms from \
5857                 every peer emit-site consumer that also routes \
5858                 through the accessor (the future wasmtime \
5859                 Store::limiter wire path, the caixa-helm \
5860                 resources.limits.memory materializer)",
5861            );
5862            // (2) Two successive validate() calls must yield the same
5863            // variant/Ok discriminant — the accessor-projected reads
5864            // and the raw-projected reads must produce identical
5865            // validation outcomes.
5866            let first = l.validate();
5867            let second = l.validate();
5868            assert_eq!(
5869                first, second,
5870                "LimitsSpec::validate must be idempotent on :memory \
5871                 {memory:?} — two successive calls must surface the \
5872                 same variant/Ok discriminant, catching any accessor \
5873                 detour that would introduce a value-dependent side \
5874                 effect on the &self projection",
5875            );
5876        }
5877        // (3) The specific arm-order shape the four converged sites
5878        // encode: `MemoryZero` (raw-`Some(0)`) precedes the page-floor
5879        // arm, which precedes the cap arm, which precedes the page-
5880        // multiple arm. Each arm must fire off the accessor-projected
5881        // read on its specific fixture value.
5882        assert_eq!(
5883            LimitsSpec {
5884                memory: Some(0),
5885                ..LimitsSpec::default()
5886            }
5887            .validate(),
5888            Err(LimitsError::MemoryZero),
5889            "MemoryZero must fire on Some(0) via the accessor projection",
5890        );
5891        assert_eq!(
5892            LimitsSpec {
5893                memory: Some(1),
5894                ..LimitsSpec::default()
5895            }
5896            .validate(),
5897            Err(LimitsError::MemoryBelowWasm32Page { bytes: 1 }),
5898            "MemoryBelowWasm32Page must fire on Some(1) via the accessor projection",
5899        );
5900        assert_eq!(
5901            LimitsSpec {
5902                memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES + 1),
5903                ..LimitsSpec::default()
5904            }
5905            .validate(),
5906            Err(LimitsError::MemoryExceedsWasm32Cap {
5907                bytes: LIMITS_MEMORY_WASM32_MAX_BYTES + 1
5908            }),
5909            "MemoryExceedsWasm32Cap must fire on one-past-cap via the accessor projection",
5910        );
5911        assert_eq!(
5912            LimitsSpec {
5913                memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES + 1),
5914                ..LimitsSpec::default()
5915            }
5916            .validate(),
5917            Err(LimitsError::MemoryNotPageMultiple {
5918                bytes: LIMITS_MEMORY_WASM32_PAGE_BYTES + 1
5919            }),
5920            "MemoryNotPageMultiple must fire on one-past-page-floor via the accessor projection",
5921        );
5922        assert_eq!(
5923            LimitsSpec {
5924                memory: Some(LIMITS_MEMORY_WASM32_PAGE_BYTES),
5925                ..LimitsSpec::default()
5926            }
5927            .validate(),
5928            Ok(()),
5929            "at-page-floor must pass validate via the accessor projection",
5930        );
5931        assert_eq!(
5932            LimitsSpec {
5933                memory: Some(LIMITS_MEMORY_WASM32_MAX_BYTES),
5934                ..LimitsSpec::default()
5935            }
5936            .validate(),
5937            Ok(()),
5938            "at-cap must pass validate via the accessor projection",
5939        );
5940    }
5941
5942    // ── per-`:limits :fuel` accessor pins (LimitsSpec::fuel) ─────────
5943
5944    #[test]
5945    fn limits_fuel_returns_option_u64_byte_equal_across_permutations() {
5946        // The canonical per-`:limits` `:fuel` wasmtime-per-call
5947        // wasm-instruction budget scalar pin: [`LimitsSpec::fuel`]
5948        // must return the `:limits :fuel` typed `u64` verbatim as an
5949        // `Option<u64>`, byte-equal to the raw field access across
5950        // the three canonical shape-arms — `None` (no fuel budget
5951        // declared — engine-default applies), `Some(1)` (the
5952        // structural minimum a validated `:limits :fuel` may carry,
5953        // one wasm instruction; wasmtime traps the first instruction
5954        // at `fuel=0`, so `Some(1)` is the smallest budget that
5955        // executes any code), `Some(1_000_000)` (the canonical 10⁶
5956        // fuel-unit budget the in-tree `Caixa::template` and the
5957        // wasmtime book's `Store::set_fuel(1_000_000)` example both
5958        // carry).
5959        //
5960        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
5961        // (620c067) accessor byte-equality pin on the peer typed-`u64`
5962        // optional-scalar axis, extended to the wasm-instruction-budget
5963        // shape — second `Option<Copy-T>`-return accessor on the M2
5964        // slot family. Pins against a future silent detour that
5965        // re-derived the fuel budget from a peer axis (an accidental
5966        // `.memory`-collapse that assumed the two `Option<u64>` axes
5967        // carry the same value — the two axes share a shape but not
5968        // a semantic, `:memory` counts linear-memory bytes and `:fuel`
5969        // counts wasm instructions), a `None` → `Some(0)` "zero means
5970        // unbounded" collapse (the canonical `Option<u64>` → `u64`
5971        // collapse footgun the [`LimitsError::FuelZero`] validate arm
5972        // guards on the peer zero-floor axis; wasmtime interprets
5973        // `fuel=0` as "trap the first instruction" not "no bound"), or
5974        // a per-arm variant swap that landed on one consumer without
5975        // the other.
5976        for fuel in [None, Some(1_u64), Some(1_000_000_u64)] {
5977            let l = LimitsSpec {
5978                fuel,
5979                ..LimitsSpec::default()
5980            };
5981            assert_eq!(
5982                l.fuel(),
5983                fuel,
5984                "LimitsSpec::fuel must return :limits :fuel verbatim \
5985                 (got {:?}, expected {fuel:?})",
5986                l.fuel(),
5987            );
5988            assert_eq!(
5989                l.fuel(),
5990                l.fuel,
5991                "LimitsSpec::fuel must byte-equal the raw .fuel \
5992                 field access across every value in the accept-set",
5993            );
5994        }
5995    }
5996
5997    #[test]
5998    fn limits_is_empty_fuel_arm_routes_through_accessor() {
5999        // Composition pin: [`LimitsSpec::is_empty`]'s `fuel` arm
6000        // must key off [`LimitsSpec::fuel`], not the raw `.fuel`
6001        // field access. Structurally: setting ONLY the `fuel` slot
6002        // on an otherwise-default LimitsSpec must flip `is_empty()`
6003        // from `true` (all-`None`) to `false` (one axis carries a
6004        // value); the flip must be observed across every value in
6005        // the accept-set since the emptiness semantic reads "any
6006        // axis carries a value" — not "any axis carries a value
6007        // above a threshold" — the same non-collapsing shape the
6008        // sibling M3 [`crate::MeshPolicy::is_empty`] predicate
6009        // carries on its peer `Option<Copy-T>`-typed slot surfaces
6010        // and the sibling per-`:limits` [`LimitsSpec::memory`]
6011        // (620c067) `is_empty()` accessor-composition pin carries on
6012        // the peer `Option<u64>` axis.
6013        //
6014        // Pins against a future silent detour that re-derived the
6015        // emptiness predicate off a peer axis (an accidental
6016        // `.memory.is_none()`-only chain that dropped the `fuel` arm
6017        // entirely), an accessor-side detour that no longer names the
6018        // substrate-primitive typed dispatch (an accidental
6019        // `self.fuel.unwrap_or(0) == 0` fallback in the accessor
6020        // that would silently classify both `None` and `Some(0)` as
6021        // the same value — a footgun the [`LimitsError::FuelZero`]
6022        // validate arm explicitly closes since `fuel=0` traps rather
6023        // than expresses "unbounded"), or a threshold collapse (a
6024        // `self.fuel().is_some_and(|f| f > 0)` that would silently
6025        // classify `Some(0)` as unset).
6026        //
6027        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6028        // (620c067) `is_empty` composition pin on the peer
6029        // `Option<u64>` axis — same "the emptiness predicate must
6030        // route through the substrate-primitive typed dispatch"
6031        // discipline extended onto the peer per-`:limits` `:fuel`
6032        // arm.
6033        let empty = LimitsSpec::default();
6034        assert!(
6035            empty.is_empty(),
6036            "LimitsSpec::default() must be is_empty() — every axis \
6037             defaults to None",
6038        );
6039        for fuel in [Some(1_u64), Some(1_000_000_u64), Some(LIMITS_FUEL_MAX)] {
6040            let l = LimitsSpec {
6041                fuel,
6042                ..LimitsSpec::default()
6043            };
6044            assert!(
6045                !l.is_empty(),
6046                "LimitsSpec::is_empty must return false when :fuel \
6047                 is {fuel:?} — the emptiness predicate reads \"any \
6048                 axis carries a value\", not \"any axis carries a \
6049                 value above a threshold\"",
6050            );
6051            assert_eq!(
6052                l.fuel().is_none(),
6053                l.is_empty(),
6054                "when :fuel is the only set axis, is_empty() must \
6055                 equal fuel().is_none() — the accessor and the \
6056                 emptiness predicate must route through the same \
6057                 substrate-primitive typed dispatch on the :fuel \
6058                 arm",
6059            );
6060        }
6061    }
6062
6063    #[test]
6064    fn limits_fuel_projects_option_u64_by_copy() {
6065        // The by-copy pin: [`LimitsSpec::fuel`] returns `Option<u64>`
6066        // by copy — `Option<u64>` is `Copy` and the accessor must
6067        // return by value, not by reference. Peer of the sibling per-
6068        // `:limits` [`LimitsSpec::memory`] (620c067) copy-invariant
6069        // pin on the peer `Option<u64>` shape — the accessor's
6070        // returned `Option<u64>` must outlive `&self` (multiple calls
6071        // must return equal values from a dropped-`&self` copy, since
6072        // the returned Option carries no borrow), and calling the
6073        // accessor twice on the same LimitsSpec must yield the same
6074        // `Option<u64>` verbatim (idempotent, no side effects on
6075        // `&self`).
6076        //
6077        // Pins against a future silent detour that returned
6078        // `Option<&u64>` (which would type-check but silently break
6079        // every downstream caller — the future `wasmtime::Store::set_fuel`
6080        // wire path consumes `u64` by value and `&u64` would fold to
6081        // a detached copy at the call site), an accidental
6082        // `Option::as_ref()` projection (`self.fuel.as_ref()` would
6083        // also type-check but return `Option<&u64>`), or a one-arm-
6084        // only accessor that reads `Some(*f)` in the Some arm but
6085        // reads a fresh `Default::default()` in the None arm.
6086        for fuel in [
6087            None,
6088            Some(1_u64),
6089            Some(1_000_000_u64),
6090            Some(LIMITS_FUEL_MAX),
6091        ] {
6092            let l = LimitsSpec {
6093                fuel,
6094                ..LimitsSpec::default()
6095            };
6096            let first = l.fuel();
6097            let second = l.fuel();
6098            assert_eq!(
6099                first, second,
6100                "LimitsSpec::fuel must be idempotent — two \
6101                 successive calls on the same &self must return the \
6102                 same Option<u64>",
6103            );
6104            assert_eq!(
6105                first, fuel,
6106                "LimitsSpec::fuel must return :limits :fuel \
6107                 verbatim by copy — got {first:?}, expected {fuel:?}",
6108            );
6109        }
6110    }
6111
6112    // ── per-`:limits :wall-clock` accessor pins (LimitsSpec::wall_clock) ─
6113
6114    #[test]
6115    fn limits_wall_clock_returns_option_duration_byte_equal_across_permutations() {
6116        // The canonical per-`:limits` `:wall-clock` wasmtime-per-call
6117        // wall-clock deadline scalar pin: [`LimitsSpec::wall_clock`]
6118        // must return the `:limits :wall-clock` typed `Duration`
6119        // verbatim as an `Option<Duration>`, byte-equal to the raw
6120        // field access across the three canonical shape-arms — `None`
6121        // (no wall-clock deadline declared — engine-default applies),
6122        // `Some(Duration::from_millis(1))` (the structural minimum a
6123        // validated `:limits :wall-clock` may carry, the
6124        // integer-millisecond floor
6125        // [`LimitsError::WallClockNotCanonical`] rejects everything
6126        // sub-ms; `Duration::ZERO` is separately rejected by
6127        // [`LimitsError::WallClockZero`]), `Some(Duration::from_secs(30))`
6128        // (the canonical 30s deadline the module-level docstring
6129        // names).
6130        //
6131        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6132        // (620c067) / [`LimitsSpec::fuel`] (795dee7) accessor
6133        // byte-equality pins on the peer typed-`u64` optional-scalar
6134        // axes, extended to the wall-clock-deadline `Option<Duration>`
6135        // shape — third `Option<Copy-T>`-return accessor on the M2 slot
6136        // family. Sibling to [`crate::MeshPolicy::timeout`] (7073d0f) on
6137        // the M3 mesh-slot family's peer `Option<Duration>` accessor
6138        // axis — same typed-`Duration` shape extended from the M3
6139        // per-call-timeout axis to the M2 per-outermost-call-deadline
6140        // axis. Pins against a future silent detour that re-derived the
6141        // wall-clock deadline from a peer axis (an accidental
6142        // `.fuel`-collapse that assumed the wall-clock deadline and
6143        // the fuel budget carry the same value — the two axes serve
6144        // different sandboxing purposes, wall-clock tracks scheduler
6145        // real time and fuel tracks wasm instructions), a `None` →
6146        // `Some(Duration::ZERO)` "zero means unbounded" collapse (the
6147        // canonical `Option<Duration>` → `Duration` collapse footgun
6148        // the [`LimitsError::WallClockZero`] validate arm guards on the
6149        // peer zero-floor axis; a zero deadline traps the first
6150        // instruction), or a per-arm variant swap that landed on one
6151        // consumer without the other.
6152        for wall_clock in [
6153            None,
6154            Some(Duration::from_millis(1)),
6155            Some(Duration::from_secs(30)),
6156        ] {
6157            let l = LimitsSpec {
6158                wall_clock,
6159                ..LimitsSpec::default()
6160            };
6161            assert_eq!(
6162                l.wall_clock(),
6163                wall_clock,
6164                "LimitsSpec::wall_clock must return :limits :wall-clock verbatim \
6165                 (got {:?}, expected {wall_clock:?})",
6166                l.wall_clock(),
6167            );
6168            assert_eq!(
6169                l.wall_clock(),
6170                l.wall_clock,
6171                "LimitsSpec::wall_clock must byte-equal the raw .wall_clock \
6172                 field access across every value in the accept-set",
6173            );
6174        }
6175    }
6176
6177    #[test]
6178    fn limits_is_empty_wall_clock_arm_routes_through_accessor() {
6179        // Composition pin: [`LimitsSpec::is_empty`]'s `wall_clock` arm
6180        // must key off [`LimitsSpec::wall_clock`], not the raw
6181        // `.wall_clock` field access. Structurally: setting ONLY the
6182        // `wall_clock` slot on an otherwise-default LimitsSpec must
6183        // flip `is_empty()` from `true` (all-`None`) to `false` (one
6184        // axis carries a value); the flip must be observed across every
6185        // value in the accept-set since the emptiness semantic reads
6186        // "any axis carries a value" — not "any axis carries a value
6187        // above a threshold" — the same non-collapsing shape the
6188        // sibling M3 [`crate::MeshPolicy::is_empty`] predicate carries
6189        // on its peer `Option<Copy-T>`-typed slot surfaces and the
6190        // sibling per-`:limits` [`LimitsSpec::memory`] (620c067) /
6191        // [`LimitsSpec::fuel`] (795dee7) `is_empty()` accessor-
6192        // composition pins carry on the peer `Option<u64>` axes.
6193        //
6194        // Pins against a future silent detour that re-derived the
6195        // emptiness predicate off a peer axis (an accidental
6196        // `.memory.is_none()`-only chain that dropped the `wall_clock`
6197        // arm entirely), an accessor-side detour that no longer names
6198        // the substrate-primitive typed dispatch (an accidental
6199        // `self.wall_clock.unwrap_or(Duration::ZERO).is_zero()` fallback
6200        // in the accessor that would silently classify both `None` and
6201        // `Some(Duration::ZERO)` as the same value — a footgun the
6202        // [`LimitsError::WallClockZero`] validate arm explicitly closes
6203        // since a zero deadline traps rather than expresses
6204        // "unbounded"), or a threshold collapse (a
6205        // `self.wall_clock().is_some_and(|w| !w.is_zero())` that would
6206        // silently classify `Some(Duration::ZERO)` as unset).
6207        //
6208        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6209        // (620c067) / [`LimitsSpec::fuel`] (795dee7) `is_empty`
6210        // composition pins on the peer `Option<u64>` axes — same "the
6211        // emptiness predicate must route through the substrate-
6212        // primitive typed dispatch" discipline extended onto the peer
6213        // per-`:limits` `:wall-clock` arm.
6214        let empty = LimitsSpec::default();
6215        assert!(
6216            empty.is_empty(),
6217            "LimitsSpec::default() must be is_empty() — every axis \
6218             defaults to None",
6219        );
6220        for wall_clock in [
6221            Some(Duration::from_millis(1)),
6222            Some(Duration::from_secs(30)),
6223            Some(LIMITS_WALL_CLOCK_MAX),
6224        ] {
6225            let l = LimitsSpec {
6226                wall_clock,
6227                ..LimitsSpec::default()
6228            };
6229            assert!(
6230                !l.is_empty(),
6231                "LimitsSpec::is_empty must return false when :wall-clock \
6232                 is {wall_clock:?} — the emptiness predicate reads \"any \
6233                 axis carries a value\", not \"any axis carries a \
6234                 value above a threshold\"",
6235            );
6236            assert_eq!(
6237                l.wall_clock().is_none(),
6238                l.is_empty(),
6239                "when :wall-clock is the only set axis, is_empty() must \
6240                 equal wall_clock().is_none() — the accessor and the \
6241                 emptiness predicate must route through the same \
6242                 substrate-primitive typed dispatch on the :wall-clock \
6243                 arm",
6244            );
6245        }
6246    }
6247
6248    #[test]
6249    fn limits_wall_clock_projects_option_duration_by_copy() {
6250        // The by-copy pin: [`LimitsSpec::wall_clock`] returns
6251        // `Option<Duration>` by copy — `Duration` is `Copy` (so
6252        // `Option<Duration>` is `Copy`) and the accessor must return by
6253        // value, not by reference. Peer of the sibling per-`:limits`
6254        // [`LimitsSpec::memory`] (620c067) / [`LimitsSpec::fuel`]
6255        // (795dee7) copy-invariant pins on the peer `Option<u64>`
6256        // shape, extended onto the peer `Option<Duration>` shape — the
6257        // accessor's returned `Option<Duration>` must outlive `&self`
6258        // (multiple calls must return equal values from a dropped-
6259        // `&self` copy, since the returned Option carries no borrow),
6260        // and calling the accessor twice on the same LimitsSpec must
6261        // yield the same `Option<Duration>` verbatim (idempotent, no
6262        // side effects on `&self`).
6263        //
6264        // Pins against a future silent detour that returned
6265        // `Option<&Duration>` (which would type-check but silently
6266        // break every downstream caller — the future
6267        // `wasmtime::Store::epoch_deadline_*` wire path consumes
6268        // `Duration` by value and `&Duration` would fold to a detached
6269        // copy at the call site), an accidental `Option::as_ref()`
6270        // projection (`self.wall_clock.as_ref()` would also type-check
6271        // but return `Option<&Duration>`), or a one-arm-only accessor
6272        // that reads `Some(*w)` in the Some arm but reads a fresh
6273        // `Default::default()` (which would collapse to
6274        // `Duration::ZERO`, not `None`) in the None arm.
6275        for wall_clock in [
6276            None,
6277            Some(Duration::from_millis(1)),
6278            Some(Duration::from_secs(30)),
6279            Some(LIMITS_WALL_CLOCK_MAX),
6280        ] {
6281            let l = LimitsSpec {
6282                wall_clock,
6283                ..LimitsSpec::default()
6284            };
6285            let first = l.wall_clock();
6286            let second = l.wall_clock();
6287            assert_eq!(
6288                first, second,
6289                "LimitsSpec::wall_clock must be idempotent — two \
6290                 successive calls on the same &self must return the \
6291                 same Option<Duration>",
6292            );
6293            assert_eq!(
6294                first, wall_clock,
6295                "LimitsSpec::wall_clock must return :limits :wall-clock \
6296                 verbatim by copy — got {first:?}, expected {wall_clock:?}",
6297            );
6298        }
6299    }
6300
6301    // ── per-`:limits :cpu` accessor pins (LimitsSpec::cpu) ───────────
6302
6303    #[test]
6304    fn limits_cpu_returns_option_u32_byte_equal_across_permutations() {
6305        // The canonical per-`:limits` `:cpu` Kubernetes-millicore
6306        // soft cgroup-share scalar pin: [`LimitsSpec::cpu`] must return
6307        // the `:limits :cpu` typed `u32` verbatim as an `Option<u32>`,
6308        // byte-equal to the raw field access across the three canonical
6309        // shape-arms — `None` (no cgroup share declared —
6310        // scheduler-default applies), `Some(1)` (the structural minimum
6311        // a validated `:limits :cpu` may carry, one millicore; a zero
6312        // cgroup share is separately rejected by
6313        // [`LimitsError::CpuZero`]), `Some(500)` (the canonical 500m
6314        // half-a-core share the in-tree
6315        // `limits_slot_propagates_into_values_block` smoke test carries
6316        // as the load-bearing example, peer to the `caixa-flux`
6317        // projector's identical 500m default).
6318        //
6319        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6320        // (620c067) / [`LimitsSpec::fuel`] (795dee7) /
6321        // [`LimitsSpec::wall_clock`] (8cb717b) accessor byte-equality
6322        // pins on the peer typed-`u64` / `u64` / `Duration`
6323        // optional-scalar axes, extended to the cgroup-cpu-share
6324        // `Option<u32>` shape — fourth and final `Option<Copy-T>`-return
6325        // accessor on the M2 slot family, closing the M2 `:limits`
6326        // `Option<Copy-T>` accessor axis. Sibling to
6327        // [`crate::MeshPolicy::retries`] (bdfb399) on the M3 mesh-slot
6328        // family's peer `Option<u32>` accessor axis — same typed-`u32`
6329        // shape extended from the M3 per-edge-transient-failure-retry-
6330        // budget axis to the M2 per-process-cgroup-cpu-share axis.
6331        // Pins against a future silent detour that re-derived the cpu
6332        // share from a peer axis (an accidental `.retries`-collapse that
6333        // assumed the two `Option<u32>` axes carry the same value — the
6334        // two axes share a shape but not a semantic, M2 `:cpu` counts
6335        // millicores of soft cgroup share and M3 `:retries` counts
6336        // per-edge transient-failure retry budget), a `None` → `Some(0)`
6337        // "zero means unbounded" collapse (the canonical `Option<u32>` →
6338        // `u32` collapse footgun the [`LimitsError::CpuZero`] validate
6339        // arm guards on the peer zero-floor axis; a zero cgroup share
6340        // starves the process rather than expressing "unbounded"), or a
6341        // per-arm variant swap that landed on one consumer without the
6342        // other.
6343        for cpu in [None, Some(1_u32), Some(500_u32)] {
6344            let l = LimitsSpec {
6345                cpu,
6346                ..LimitsSpec::default()
6347            };
6348            assert_eq!(
6349                l.cpu(),
6350                cpu,
6351                "LimitsSpec::cpu must return :limits :cpu verbatim \
6352                 (got {:?}, expected {cpu:?})",
6353                l.cpu(),
6354            );
6355            assert_eq!(
6356                l.cpu(),
6357                l.cpu,
6358                "LimitsSpec::cpu must byte-equal the raw .cpu \
6359                 field access across every value in the accept-set",
6360            );
6361        }
6362    }
6363
6364    #[test]
6365    fn limits_is_empty_cpu_arm_routes_through_accessor() {
6366        // Composition pin: [`LimitsSpec::is_empty`]'s `cpu` arm must key
6367        // off [`LimitsSpec::cpu`], not the raw `.cpu` field access.
6368        // Structurally: setting ONLY the `cpu` slot on an
6369        // otherwise-default LimitsSpec must flip `is_empty()` from
6370        // `true` (all-`None`) to `false` (one axis carries a value);
6371        // the flip must be observed across every value in the
6372        // accept-set since the emptiness semantic reads "any axis
6373        // carries a value" — not "any axis carries a value above a
6374        // threshold" — the same non-collapsing shape the sibling M3
6375        // [`crate::MeshPolicy::is_empty`] predicate carries on its
6376        // peer `Option<Copy-T>`-typed slot surfaces and the sibling
6377        // per-`:limits` [`LimitsSpec::memory`] (620c067) /
6378        // [`LimitsSpec::fuel`] (795dee7) / [`LimitsSpec::wall_clock`]
6379        // (8cb717b) `is_empty()` accessor-composition pins carry on the
6380        // peer `Option<u64>` / `Option<u64>` / `Option<Duration>` axes.
6381        //
6382        // Pins against a future silent detour that re-derived the
6383        // emptiness predicate off a peer axis (an accidental
6384        // `.memory.is_none()`-only chain that dropped the `cpu` arm
6385        // entirely), an accessor-side detour that no longer names the
6386        // substrate-primitive typed dispatch (an accidental
6387        // `self.cpu.unwrap_or(0) == 0` fallback in the accessor that
6388        // would silently classify both `None` and `Some(0)` as the same
6389        // value — a footgun the [`LimitsError::CpuZero`] validate arm
6390        // explicitly closes since a zero cgroup share starves the
6391        // process rather than expressing "unbounded"), or a threshold
6392        // collapse (a `self.cpu().is_some_and(|m| m > 0)` that would
6393        // silently classify `Some(0)` as unset).
6394        //
6395        // Peer of the sibling per-`:limits` [`LimitsSpec::memory`]
6396        // (620c067) / [`LimitsSpec::fuel`] (795dee7) /
6397        // [`LimitsSpec::wall_clock`] (8cb717b) `is_empty` composition
6398        // pins on the peer `Option<u64>` / `Option<u64>` /
6399        // `Option<Duration>` axes — same "the emptiness predicate must
6400        // route through the substrate-primitive typed dispatch"
6401        // discipline extended onto the peer per-`:limits` `:cpu` arm.
6402        // Closes the M2 `:limits` `is_empty`-composition family — every
6403        // arm now routes through its typed accessor, no open-coded
6404        // field access remains.
6405        let empty = LimitsSpec::default();
6406        assert!(
6407            empty.is_empty(),
6408            "LimitsSpec::default() must be is_empty() — every axis \
6409             defaults to None",
6410        );
6411        for cpu in [Some(1_u32), Some(500_u32), Some(LIMITS_CPU_MILLICORES_MAX)] {
6412            let l = LimitsSpec {
6413                cpu,
6414                ..LimitsSpec::default()
6415            };
6416            assert!(
6417                !l.is_empty(),
6418                "LimitsSpec::is_empty must return false when :cpu \
6419                 is {cpu:?} — the emptiness predicate reads \"any \
6420                 axis carries a value\", not \"any axis carries a \
6421                 value above a threshold\"",
6422            );
6423            assert_eq!(
6424                l.cpu().is_none(),
6425                l.is_empty(),
6426                "when :cpu is the only set axis, is_empty() must \
6427                 equal cpu().is_none() — the accessor and the \
6428                 emptiness predicate must route through the same \
6429                 substrate-primitive typed dispatch on the :cpu \
6430                 arm",
6431            );
6432        }
6433    }
6434
6435    #[test]
6436    fn limits_cpu_projects_option_u32_by_copy() {
6437        // The by-copy pin: [`LimitsSpec::cpu`] returns `Option<u32>` by
6438        // copy — `Option<u32>` is `Copy` and the accessor must return
6439        // by value, not by reference. Peer of the sibling per-`:limits`
6440        // [`LimitsSpec::memory`] (620c067) / [`LimitsSpec::fuel`]
6441        // (795dee7) / [`LimitsSpec::wall_clock`] (8cb717b)
6442        // copy-invariant pins on the peer `Option<u64>` / `Option<u64>`
6443        // / `Option<Duration>` shapes, extended onto the peer
6444        // `Option<u32>` copy-invariant shape — the accessor's returned
6445        // `Option<u32>` must outlive `&self` (multiple calls must
6446        // return equal values from a dropped-`&self` copy, since the
6447        // returned Option carries no borrow), and calling the accessor
6448        // twice on the same LimitsSpec must yield the same
6449        // `Option<u32>` verbatim (idempotent, no side effects on
6450        // `&self`).
6451        //
6452        // Pins against a future silent detour that returned
6453        // `Option<&u32>` (which would type-check but silently break
6454        // every downstream caller — the future K8s pod-spec
6455        // `resources.requests.cpu` wire path consumes `u32` by value
6456        // and `&u32` would fold to a detached copy at the call site),
6457        // an accidental `Option::as_ref()` projection
6458        // (`self.cpu.as_ref()` would also type-check but return
6459        // `Option<&u32>`), or a one-arm-only accessor that reads
6460        // `Some(*m)` in the Some arm but reads a fresh
6461        // `Default::default()` in the None arm.
6462        for cpu in [
6463            None,
6464            Some(1_u32),
6465            Some(500_u32),
6466            Some(LIMITS_CPU_MILLICORES_MAX),
6467        ] {
6468            let l = LimitsSpec {
6469                cpu,
6470                ..LimitsSpec::default()
6471            };
6472            let first = l.cpu();
6473            let second = l.cpu();
6474            assert_eq!(
6475                first, second,
6476                "LimitsSpec::cpu must be idempotent — two \
6477                 successive calls on the same &self must return the \
6478                 same Option<u32>",
6479            );
6480            assert_eq!(
6481                first, cpu,
6482                "LimitsSpec::cpu must return :limits :cpu \
6483                 verbatim by copy — got {first:?}, expected {cpu:?}",
6484            );
6485        }
6486    }
6487}