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