Skip to main content

deepstrike_core/runtime/kernel/wire/
scalar.rs

1//! Cross-language scalar and projection rules (spec §7.1.1).
2//!
3//! Every rule here exists because the same value has to mean the same thing in Rust, Python,
4//! Node and WASM:
5//!
6//! * logical `u64` travels as a **canonical decimal string** ([`WireU64`]) so a JS number can
7//!   never silently round a step sequence or a millisecond clock;
8//! * authoritative policy ratios travel as **fixed-point parts-per-million** ([`Ppm`]) so no
9//!   branch depends on a language's default float;
10//! * observation-only floats are [`FiniteF64`] — NaN/Infinity are rejected at the boundary;
11//! * canonical bytes are [`CanonicalBytes`], whose JSON projection is an **explicit** base64
12//!   envelope rather than a bare string or a number array;
13//! * identities, digests and opaque references are branded newtypes, never bare integers.
14
15use std::fmt;
16
17use serde::de::{self, Deserializer, Unexpected, Visitor};
18use serde::{Deserialize, Serialize, Serializer};
19
20/// Prefix of every scalar-rule rejection. `WireRejection` classifies on it, so all four host
21/// languages can map "this value broke a scalar rule" onto one structured error.
22pub const SCALAR_ERROR_MARKER: &str = "wire scalar rejected";
23
24/// Absolute byte bound for any branded identity on the wire.
25pub const MAX_ID_BYTES: usize = 256;
26
27/// Largest integer a IEEE-754 double represents exactly. `WireU64` values above it are exactly
28/// the reason logical `u64` never travels as a JSON number.
29pub const JS_SAFE_INTEGER_MAX: u64 = (1 << 53) - 1;
30
31/// A scalar rejected by a wire rule.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct WireScalarError {
34    pub message: String,
35}
36
37impl WireScalarError {
38    pub fn new(message: impl Into<String>) -> Self {
39        Self {
40            message: message.into(),
41        }
42    }
43}
44
45impl fmt::Display for WireScalarError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "{SCALAR_ERROR_MARKER}: {}", self.message)
48    }
49}
50
51impl std::error::Error for WireScalarError {}
52
53fn scalar_error<E: de::Error>(message: impl fmt::Display) -> E {
54    E::custom(format!("{SCALAR_ERROR_MARKER}: {message}"))
55}
56
57// ---------------------------------------------------------------------------------------------
58// WireU64
59// ---------------------------------------------------------------------------------------------
60
61/// A logical `u64` that travels as a canonical decimal string.
62///
63/// Canonical means: ASCII digits only, no sign, no radix prefix, no surrounding whitespace and
64/// no leading zeros (`"0"` is the only representation of zero). Two hosts that mean the same
65/// number therefore always produce the same bytes — a precondition for canonical record digests.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
67pub struct WireU64(u64);
68
69impl WireU64 {
70    pub const ZERO: Self = Self(0);
71
72    pub const fn new(value: u64) -> Self {
73        Self(value)
74    }
75
76    pub const fn get(self) -> u64 {
77        self.0
78    }
79
80    /// Whether this value survives a round-trip through a JS `number`. Hosts that project to
81    /// `bigint` or keep the branded decimal string never need to ask.
82    pub const fn is_js_safe(self) -> bool {
83        self.0 <= JS_SAFE_INTEGER_MAX
84    }
85
86    pub fn parse(text: &str) -> Result<Self, WireScalarError> {
87        if text.is_empty() {
88            return Err(WireScalarError::new("u64 decimal string is empty"));
89        }
90        if !text.bytes().all(|b| b.is_ascii_digit()) {
91            return Err(WireScalarError::new(format!(
92                "u64 must be a canonical decimal string, got {text:?}"
93            )));
94        }
95        if text.len() > 1 && text.starts_with('0') {
96            return Err(WireScalarError::new(format!(
97                "u64 decimal string must not have leading zeros, got {text:?}"
98            )));
99        }
100        text.parse::<u64>().map(Self).map_err(|_| {
101            WireScalarError::new(format!("u64 decimal string {text:?} is out of range"))
102        })
103    }
104}
105
106impl fmt::Display for WireU64 {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{}", self.0)
109    }
110}
111
112impl From<u64> for WireU64 {
113    fn from(value: u64) -> Self {
114        Self(value)
115    }
116}
117
118impl Serialize for WireU64 {
119    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
120        serializer.collect_str(&self.0)
121    }
122}
123
124struct WireU64Visitor;
125
126impl Visitor<'_> for WireU64Visitor {
127    type Value = WireU64;
128
129    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str("a canonical decimal string encoding a u64")
131    }
132
133    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
134        WireU64::parse(value).map_err(|err| scalar_error(err.message))
135    }
136
137    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
138        Err(scalar_error(format!(
139            "u64 must be a decimal string, got the JSON number {value}"
140        )))
141    }
142
143    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
144        Err(scalar_error(format!(
145            "u64 must be a decimal string, got the JSON number {value}"
146        )))
147    }
148
149    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
150        Err(scalar_error(format!(
151            "u64 must be a decimal string, got the JSON number {value}"
152        )))
153    }
154
155    fn visit_bool<E: de::Error>(self, value: bool) -> Result<Self::Value, E> {
156        Err(scalar_error(format!(
157            "u64 must be a decimal string, got the boolean {value}"
158        )))
159    }
160
161    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
162        Err(scalar_error("u64 must be a decimal string, got null"))
163    }
164}
165
166impl<'de> Deserialize<'de> for WireU64 {
167    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168        // `deserialize_any` (not `deserialize_str`): serde_json answers a non-string token to
169        // `deserialize_str` with its own `invalid_type` error before the visitor ever runs, which
170        // would hide the scalar rule behind a generic type error.
171        deserializer.deserialize_any(WireU64Visitor)
172    }
173}
174
175// ---------------------------------------------------------------------------------------------
176// Ppm
177// ---------------------------------------------------------------------------------------------
178
179/// A ratio in `[0, 1]` expressed as fixed-point parts-per-million.
180///
181/// Authoritative policy thresholds must not be floats: `0.25` is not representable identically in
182/// every language/serializer, and a threshold comparison that differs by one ULP is a different
183/// kernel decision. `Ppm(250_000)` is exact everywhere.
184///
185/// Ratios greater than `1.0` (e.g. an over-provisioning multiplier) need a distinct type with its
186/// own bound; deliberately not invented here before a real callsite exists.
187#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
188pub struct Ppm(u32);
189
190impl Ppm {
191    pub const ZERO: Self = Self(0);
192    pub const ONE: Self = Self(1_000_000);
193    pub const MAX_PPM: u32 = 1_000_000;
194
195    pub fn new(parts_per_million: u32) -> Result<Self, WireScalarError> {
196        if parts_per_million > Self::MAX_PPM {
197            return Err(WireScalarError::new(format!(
198                "ratio {parts_per_million} ppm exceeds 1.0 ({} ppm)",
199                Self::MAX_PPM
200            )));
201        }
202        Ok(Self(parts_per_million))
203    }
204
205    pub const fn get(self) -> u32 {
206        self.0
207    }
208
209    /// Observation/diagnostic projection only — never feed this back into a branch.
210    pub fn as_ratio(self) -> f64 {
211        f64::from(self.0) / f64::from(Self::MAX_PPM)
212    }
213
214    /// Convert a host-supplied ratio at the boundary (rounding to the nearest ppm). The float
215    /// stops here: everything downstream compares integers.
216    pub fn from_ratio(ratio: f64) -> Result<Self, WireScalarError> {
217        if !ratio.is_finite() {
218            return Err(WireScalarError::new("ratio must be finite"));
219        }
220        if !(0.0..=1.0).contains(&ratio) {
221            return Err(WireScalarError::new(format!(
222                "ratio {ratio} is outside [0.0, 1.0]"
223            )));
224        }
225        Self::new((ratio * f64::from(Self::MAX_PPM)).round() as u32)
226    }
227}
228
229impl Serialize for Ppm {
230    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
231        serializer.serialize_u32(self.0)
232    }
233}
234
235struct PpmVisitor;
236
237impl Visitor<'_> for PpmVisitor {
238    type Value = Ppm;
239
240    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        f.write_str("an integer number of parts-per-million in [0, 1000000]")
242    }
243
244    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
245        u32::try_from(value)
246            .map_err(|_| scalar_error::<E>(format!("{value} ppm is out of range")))
247            .and_then(|value| Ppm::new(value).map_err(|err| scalar_error(err.message)))
248    }
249
250    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
251        if value < 0 {
252            return Err(scalar_error(format!(
253                "ppm must not be negative, got {value}"
254            )));
255        }
256        self.visit_u64(value as u64)
257    }
258
259    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
260        Err(scalar_error(format!(
261            "policy ratios are fixed-point parts-per-million integers, got the float {value}"
262        )))
263    }
264
265    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
266        Err(scalar_error(format!(
267            "ppm must be a JSON integer, got the string {value:?}"
268        )))
269    }
270
271    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
272        Err(scalar_error("ppm must be a JSON integer, got null"))
273    }
274}
275
276impl<'de> Deserialize<'de> for Ppm {
277    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
278        deserializer.deserialize_any(PpmVisitor)
279    }
280}
281
282// ---------------------------------------------------------------------------------------------
283// FiniteF64
284// ---------------------------------------------------------------------------------------------
285
286/// An **observation-only** float. NaN and ±Infinity never cross the boundary: they are not
287/// representable in JSON, they break canonical bytes, and they turn any comparison into a
288/// silent false.
289#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
290pub struct FiniteF64(f64);
291
292impl FiniteF64 {
293    pub fn new(value: f64) -> Result<Self, WireScalarError> {
294        if !value.is_finite() {
295            return Err(WireScalarError::new(format!(
296                "observation float must be finite, got {value}"
297            )));
298        }
299        Ok(Self(value))
300    }
301
302    pub const fn get(self) -> f64 {
303        self.0
304    }
305}
306
307impl Serialize for FiniteF64 {
308    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
309        serializer.serialize_f64(self.0)
310    }
311}
312
313struct FiniteF64Visitor;
314
315impl Visitor<'_> for FiniteF64Visitor {
316    type Value = FiniteF64;
317
318    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        f.write_str("a finite JSON number")
320    }
321
322    fn visit_f64<E: de::Error>(self, value: f64) -> Result<Self::Value, E> {
323        FiniteF64::new(value).map_err(|err| scalar_error(err.message))
324    }
325
326    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
327        FiniteF64::new(value as f64).map_err(|err| scalar_error(err.message))
328    }
329
330    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
331        FiniteF64::new(value as f64).map_err(|err| scalar_error(err.message))
332    }
333
334    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
335        Err(scalar_error(format!(
336            "observation float must be a JSON number, got the string {value:?}"
337        )))
338    }
339}
340
341impl<'de> Deserialize<'de> for FiniteF64 {
342    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
343        deserializer.deserialize_any(FiniteF64Visitor)
344    }
345}
346
347// ---------------------------------------------------------------------------------------------
348// CanonicalBytes
349// ---------------------------------------------------------------------------------------------
350
351/// Canonical record/checkpoint bytes.
352///
353/// Native bindings project this to `bytes` / `Uint8Array`. The JSON projection used by
354/// diagnostics and exports is **explicit**: `{"encoding":"base64","data":"…"}`. A bare string
355/// would be indistinguishable from text, and a number array would double in size and invite
356/// per-language element typing.
357#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
358pub struct CanonicalBytes(Vec<u8>);
359
360impl CanonicalBytes {
361    pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
362        Self(bytes.into())
363    }
364
365    pub fn as_slice(&self) -> &[u8] {
366        &self.0
367    }
368
369    pub fn into_vec(self) -> Vec<u8> {
370        self.0
371    }
372
373    pub fn len(&self) -> usize {
374        self.0.len()
375    }
376
377    pub fn is_empty(&self) -> bool {
378        self.0.is_empty()
379    }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "snake_case")]
384enum BytesEncoding {
385    Base64,
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
389#[serde(deny_unknown_fields)]
390struct CanonicalBytesProjection {
391    encoding: BytesEncoding,
392    data: String,
393}
394
395impl Serialize for CanonicalBytes {
396    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
397        CanonicalBytesProjection {
398            encoding: BytesEncoding::Base64,
399            data: base64_encode(&self.0),
400        }
401        .serialize(serializer)
402    }
403}
404
405impl<'de> Deserialize<'de> for CanonicalBytes {
406    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
407        let projection = CanonicalBytesProjection::deserialize(deserializer)?;
408        base64_decode(&projection.data)
409            .map(Self)
410            .map_err(|err| scalar_error(err.message))
411    }
412}
413
414const BASE64_ALPHABET: &[u8; 64] =
415    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
416
417fn base64_encode(bytes: &[u8]) -> String {
418    let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
419    for chunk in bytes.chunks(3) {
420        let b0 = chunk[0] as u32;
421        let b1 = *chunk.get(1).unwrap_or(&0) as u32;
422        let b2 = *chunk.get(2).unwrap_or(&0) as u32;
423        let triple = (b0 << 16) | (b1 << 8) | b2;
424        out.push(BASE64_ALPHABET[(triple >> 18) as usize & 0x3f] as char);
425        out.push(BASE64_ALPHABET[(triple >> 12) as usize & 0x3f] as char);
426        out.push(if chunk.len() > 1 {
427            BASE64_ALPHABET[(triple >> 6) as usize & 0x3f] as char
428        } else {
429            '='
430        });
431        out.push(if chunk.len() > 2 {
432            BASE64_ALPHABET[triple as usize & 0x3f] as char
433        } else {
434            '='
435        });
436    }
437    out
438}
439
440fn base64_value(byte: u8) -> Option<u32> {
441    match byte {
442        b'A'..=b'Z' => Some(u32::from(byte - b'A')),
443        b'a'..=b'z' => Some(u32::from(byte - b'a') + 26),
444        b'0'..=b'9' => Some(u32::from(byte - b'0') + 52),
445        b'+' => Some(62),
446        b'/' => Some(63),
447        _ => None,
448    }
449}
450
451/// Strict, canonical base64: standard alphabet, mandatory padding, no whitespace, no trailing
452/// bits. Anything else is a different byte string in some other decoder — which is exactly the
453/// ambiguity canonical bytes exist to remove.
454fn base64_decode(text: &str) -> Result<Vec<u8>, WireScalarError> {
455    let bytes = text.as_bytes();
456    if !bytes.len().is_multiple_of(4) {
457        return Err(WireScalarError::new(
458            "base64 payload length must be a multiple of 4 (canonical padding)",
459        ));
460    }
461    let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
462    for (index, chunk) in bytes.chunks(4).enumerate() {
463        let is_last = index == bytes.len() / 4 - 1;
464        let pad = chunk.iter().filter(|&&b| b == b'=').count();
465        if pad > 0 && !is_last {
466            return Err(WireScalarError::new(
467                "base64 padding may only end the payload",
468            ));
469        }
470        if pad > 2 || (pad > 0 && chunk[3] != b'=') || (pad == 2 && chunk[2] != b'=') {
471            return Err(WireScalarError::new("malformed base64 padding"));
472        }
473        let mut triple = 0u32;
474        for (position, &byte) in chunk.iter().enumerate() {
475            let value = if byte == b'=' {
476                0
477            } else {
478                base64_value(byte).ok_or_else(|| {
479                    WireScalarError::new(format!("illegal base64 character {:?}", byte as char))
480                })?
481            };
482            triple |= value << (18 - 6 * position);
483        }
484        out.push((triple >> 16) as u8);
485        if pad < 2 {
486            out.push((triple >> 8) as u8);
487        }
488        if pad < 1 {
489            out.push(triple as u8);
490        }
491    }
492    Ok(out)
493}
494
495// ---------------------------------------------------------------------------------------------
496// BoundedJson
497// ---------------------------------------------------------------------------------------------
498
499/// Maximum nesting depth of an opaque JSON payload carried on the wire.
500///
501/// Keep this aligned with the runtime's absolute envelope depth. Tool parameters are JSON Schema
502/// documents, and a valid schema can naturally exceed sixteen levels before any model-authored
503/// arguments exist. The envelope preflight still enforces this same finite ceiling over the whole
504/// input, so widening the scalar-local guard does not create an unbounded parse path.
505pub const BOUNDED_JSON_MAX_DEPTH: usize = 64;
506/// Maximum number of entries in any single container of an opaque JSON payload.
507pub const BOUNDED_JSON_MAX_ENTRIES: usize = 1024;
508
509/// Opaque, host-supplied JSON (signal payloads, task metadata) with a bound on how much of it
510/// the kernel is willing to carry. Unbounded free-form JSON is the one shape that can defeat
511/// every downstream size budget, so the bound lives at the boundary type, not at each callsite.
512#[derive(Debug, Clone, Default, PartialEq)]
513pub struct BoundedJson(serde_json::Value);
514
515impl BoundedJson {
516    pub fn new(value: serde_json::Value) -> Result<Self, WireScalarError> {
517        validate_bounded(&value, 1)?;
518        Ok(Self(value))
519    }
520
521    pub fn null() -> Self {
522        Self(serde_json::Value::Null)
523    }
524
525    pub fn get(&self) -> &serde_json::Value {
526        &self.0
527    }
528
529    pub fn into_value(self) -> serde_json::Value {
530        self.0
531    }
532
533    pub fn is_null(&self) -> bool {
534        self.0.is_null()
535    }
536}
537
538fn validate_bounded(value: &serde_json::Value, depth: usize) -> Result<(), WireScalarError> {
539    if depth > BOUNDED_JSON_MAX_DEPTH {
540        return Err(WireScalarError::new(format!(
541            "payload nests deeper than {BOUNDED_JSON_MAX_DEPTH}"
542        )));
543    }
544    match value {
545        serde_json::Value::Array(items) => {
546            if items.len() > BOUNDED_JSON_MAX_ENTRIES {
547                return Err(WireScalarError::new(format!(
548                    "payload container has {} entries; the bound is {BOUNDED_JSON_MAX_ENTRIES}",
549                    items.len()
550                )));
551            }
552            items
553                .iter()
554                .try_for_each(|item| validate_bounded(item, depth + 1))
555        }
556        serde_json::Value::Object(map) => {
557            if map.len() > BOUNDED_JSON_MAX_ENTRIES {
558                return Err(WireScalarError::new(format!(
559                    "payload container has {} entries; the bound is {BOUNDED_JSON_MAX_ENTRIES}",
560                    map.len()
561                )));
562            }
563            map.values()
564                .try_for_each(|item| validate_bounded(item, depth + 1))
565        }
566        serde_json::Value::Number(number) => {
567            // serde_json can produce a non-finite f64 from an out-of-range literal; §7.1.1 says
568            // no non-finite float crosses the boundary, opaque payload or not.
569            match number.as_f64() {
570                Some(float) if !float.is_finite() => {
571                    Err(WireScalarError::new("payload contains a non-finite number"))
572                }
573                _ => Ok(()),
574            }
575        }
576        _ => Ok(()),
577    }
578}
579
580impl Serialize for BoundedJson {
581    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
582        self.0.serialize(serializer)
583    }
584}
585
586impl<'de> Deserialize<'de> for BoundedJson {
587    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
588        let value = serde_json::Value::deserialize(deserializer)?;
589        Self::new(value).map_err(|err| scalar_error(err.message))
590    }
591}
592
593// ---------------------------------------------------------------------------------------------
594// branded identities
595// ---------------------------------------------------------------------------------------------
596
597fn validate_id(label: &'static str, value: &str) -> Result<(), WireScalarError> {
598    if value.is_empty() {
599        return Err(WireScalarError::new(format!("{label} must not be empty")));
600    }
601    if value.len() > MAX_ID_BYTES {
602        return Err(WireScalarError::new(format!(
603            "{label} is {} bytes; the bound is {MAX_ID_BYTES}",
604            value.len()
605        )));
606    }
607    if value.chars().any(char::is_control) {
608        return Err(WireScalarError::new(format!(
609            "{label} must not contain control characters"
610        )));
611    }
612    Ok(())
613}
614
615macro_rules! wire_id {
616    ($(#[$doc:meta])* $name:ident, $label:literal) => {
617        $(#[$doc])*
618        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
619        pub struct $name(String);
620
621        impl $name {
622            pub fn new(value: impl Into<String>) -> Result<Self, WireScalarError> {
623                let value = value.into();
624                validate_id($label, &value)?;
625                Ok(Self(value))
626            }
627
628            pub fn as_str(&self) -> &str {
629                &self.0
630            }
631
632            pub fn into_string(self) -> String {
633                self.0
634            }
635        }
636
637        impl fmt::Display for $name {
638            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639                f.write_str(&self.0)
640            }
641        }
642
643        impl Serialize for $name {
644            fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
645                serializer.serialize_str(&self.0)
646            }
647        }
648
649        impl<'de> Deserialize<'de> for $name {
650            fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
651                struct IdVisitor;
652
653                impl Visitor<'_> for IdVisitor {
654                    type Value = $name;
655
656                    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657                        f.write_str(concat!("a non-empty ", $label, " string"))
658                    }
659
660                    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
661                        $name::new(value).map_err(|err| scalar_error(err.message))
662                    }
663
664                    fn visit_u64<E: de::Error>(self, value: u64) -> Result<Self::Value, E> {
665                        Err(scalar_error(format_args!(
666                            "{} must be a branded string, got the number {}",
667                            $label,
668                            Unexpected::Unsigned(value)
669                        )))
670                    }
671
672                    fn visit_i64<E: de::Error>(self, value: i64) -> Result<Self::Value, E> {
673                        Err(scalar_error(format_args!(
674                            "{} must be a branded string, got the number {}",
675                            $label,
676                            Unexpected::Signed(value)
677                        )))
678                    }
679
680                    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
681                        Err(scalar_error(concat!($label, " must be a branded string, got null")))
682                    }
683                }
684
685                deserializer.deserialize_any(IdVisitor)
686            }
687        }
688    };
689}
690
691wire_id!(
692    /// Identity of one kernel operation. Minted once, immutable after the first accepted input.
693    OperationId,
694    "operation id"
695);
696wire_id!(
697    /// Caller-suppliable idempotency key for one envelope (DEC-2). Retrying the same intent with
698    /// the same `input_id` must reach the same durable record.
699    InputId,
700    "input id"
701);
702wire_id!(
703    /// Kernel-minted identity of one pending effect.
704    EffectId,
705    "effect id"
706);
707wire_id!(
708    /// Logical tool/provider call identity.
709    CallId,
710    "call id"
711);
712wire_id!(
713    /// Logical task identity. Never a host session id.
714    TaskId,
715    "task id"
716);
717wire_id!(
718    /// One execution attempt of a logical task.
719    AttemptId,
720    "attempt id"
721);
722wire_id!(
723    /// Logical workflow identity.
724    WorkflowId,
725    "workflow id"
726);
727wire_id!(
728    /// Node identity inside a workflow DAG.
729    NodeId,
730    "node id"
731);
732wire_id!(
733    /// Logical signal identity.
734    SignalId,
735    "signal id"
736);
737wire_id!(
738    /// Host delivery identity for one signal delivery attempt.
739    DeliveryId,
740    "delivery id"
741);
742wire_id!(
743    /// P3 context handle identity.
744    HandleId,
745    "handle id"
746);
747wire_id!(
748    /// Opaque memory access binding. Never a tenant, namespace or path.
749    MemoryBindingId,
750    "memory binding id"
751);
752
753// ---------------------------------------------------------------------------------------------
754// Ppm const construction (appended for Task 5)
755// ---------------------------------------------------------------------------------------------
756
757impl Ppm {
758    /// `const`-constructible ppm for compile-time baselines.
759    ///
760    /// [`Ppm::new`] returns a `Result` and therefore cannot appear in a `const` initialiser, but
761    /// the kernel's default policy table *is* a compile-time constant. Values above
762    /// [`Ppm::MAX_PPM`] saturate rather than panic: a default table that aborts the process at
763    /// startup would turn a typo into an outage, and saturation is still a legal ratio.
764    pub const fn from_ppm_const(parts_per_million: u32) -> Self {
765        if parts_per_million > Self::MAX_PPM {
766            Self(Self::MAX_PPM)
767        } else {
768            Self(parts_per_million)
769        }
770    }
771}