Skip to main content

uor_addr_1/
model.rs

1//! `AddressModel` — `uor-addr-1`'s `PrismModel<H, B, A, R>` declaration
2//! (wiki ADR-020 + ADR-036).
3//!
4//! The address-derivation inference is end-to-end prism: foundation's
5//! catamorphism evaluates the ψ-chain verb arena
6//! ([`crate::verbs::address_inference`]) dispatching each
7//! resolver-bound ψ-Term through
8//! [`crate::resolvers::AddressResolverTuple`]. There is no
9//! σ-enumeration, no AxisInvocation in the verb body, no algorithmic
10//! body in the typed-iso surface; the model declares the typed feature
11//! hierarchy and the parametric tensor-algebra composition that
12//! observes it.
13//!
14//! ## Typed feature hierarchy
15//!
16//! - [`JsonInput`] — the canonical-form JCS+NFC JSON byte sequence
17//!   (variable length, capped at 4096 bytes per
18//!   [`crate::shapes::AddrBounds`]'s `TERM_VALUE_MAX_BYTES`
19//!   constant — see the `HostBounds` trait impl). The
20//!   PrismModel's `Input` type.
21//! - [`AddressLabel`] — the ψ-pipeline label (71 W8 sites — the
22//!   wire-format `sha256:<64hex>` width). The PrismModel's `Output`
23//!   type.
24//!
25//! ## Wiki commitments validated
26//!
27//! - **ADR-020 PrismModel** — the four-position parametric model
28//!   declaration `<H: HostTypes, B: HostBounds, A: AxisTuple + Hasher,
29//!   R: ResolverTuple>` is realised by [`AddressModel`] via the
30//!   `prism_model!` SDK macro.
31//! - **ADR-027 sealed Output shapes** — [`AddressLabel`] is emitted via
32//!   `output_shape!`, which the wiki commits to as the canonical
33//!   sanctioned-construction path for `GroundedShape` types
34//!   (`12-Glossary.md`).
35//! - **Algebraic-closure encoding (ADR-024 / ADR-026)** — `AddressLabel`
36//!   declares 71 disjoint `ConstraintRef::Site` constraints satisfying
37//!   `χ(N(C)) = 71 = SITE_COUNT` and `β_k = 0` for k ≥ 1; the criterion
38//!   is asserted at compile time in [`crate::resolvers`].
39//! - **ADR-023 IntoBindingValue** — [`JsonInput`] carries variable
40//!   length up to `MAX_BYTES` through the catamorphism's
41//!   binding-table form per ADR-023's canonical
42//!   content-addressable byte-sequence requirement.
43
44extern crate alloc;
45
46use alloc::vec::Vec;
47
48use uor_foundation::enforcement::ShapeViolation;
49use uor_foundation::pipeline::{ConstrainedTypeShape, ConstraintRef, IntoBindingValue};
50use uor_foundation::{DefaultHostTypes, ViolationKind};
51use uor_foundation_sdk::{output_shape, prism_model};
52
53use crate::resolvers::AddressResolverTuple;
54use crate::shapes::bounds::AddrBounds;
55use crate::shapes::hasher::Sha256Hasher;
56
57// Bring the verb's term-arena const + marker fn into scope.
58#[allow(unused_imports)]
59use crate::verbs::{address_inference, VERB_TERMS_ADDRESS_INFERENCE};
60
61/// The wire-format address byte width: `sha256:` (7) + 64-hex (64) = 71.
62pub const ADDRESS_LABEL_BYTES: usize = 71;
63
64/// The maximum byte width of a canonical-form JCS+NFC JSON payload.
65/// Sized to fit within `AddrBounds`'s `TERM_VALUE_MAX_BYTES` (4096)
66/// after the ψ-stage carrier's 4-byte length prefix and 91-byte
67/// geometry-tail header are accounted for. 3968 bytes = 4 KiB - 128
68/// (4 prefix + 91 tail + 33 headroom).
69pub const JSON_INPUT_MAX_BYTES: usize = 3968;
70
71// ─── Input shape: JsonInput ─────────────────────────────────────────────
72
73/// The canonical-form JSON byte sequence produced by the host-boundary
74/// [`crate::ops::canonicalize::jcs_nfc`] transform. Variable length,
75/// capped at [`JSON_INPUT_MAX_BYTES`].
76///
77/// The byte buffer is heap-allocated for ergonomic construction in the
78/// host pipeline; on the foundation side, only `into_binding_bytes`'s
79/// `out: &mut [u8]` is observed, so the runtime allocation does not
80/// enter the typed-iso surface or any resolver carrier.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct JsonInput {
83    /// Canonical-form bytes (JCS-RFC8785 + Unicode-NFC). Length ≤
84    /// [`JSON_INPUT_MAX_BYTES`]; enforced at construction.
85    pub bytes: Vec<u8>,
86}
87
88impl JsonInput {
89    const LENGTH_VIOLATION: ShapeViolation = ShapeViolation {
90        shape_iri: "https://uor.foundation/addr/JsonInput",
91        constraint_iri: "https://uor.foundation/addr/JsonInput/maxBytes",
92        property_iri: "https://uor.foundation/addr/JsonInput/byteCount",
93        expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
94        min_count: 0,
95        max_count: JSON_INPUT_MAX_BYTES as u32,
96        kind: ViolationKind::ValueCheck,
97    };
98
99    /// Construct from canonical-form bytes.
100    ///
101    /// # Errors
102    ///
103    /// Returns [`Self::LENGTH_VIOLATION`] when `bytes.len() >
104    /// JSON_INPUT_MAX_BYTES`.
105    pub fn new(bytes: Vec<u8>) -> Result<Self, ShapeViolation> {
106        if bytes.len() > JSON_INPUT_MAX_BYTES {
107            return Err(Self::LENGTH_VIOLATION);
108        }
109        Ok(Self { bytes })
110    }
111}
112
113impl ConstrainedTypeShape for JsonInput {
114    const IRI: &'static str = "https://uor.foundation/addr/JsonInput";
115    // `SITE_COUNT` is the *maximum* byte width: each site is one W8 byte
116    // of the canonical-form payload. Actual length is variable up to
117    // this ceiling; `into_binding_bytes` returns the written length.
118    const SITE_COUNT: usize = JSON_INPUT_MAX_BYTES;
119    const CONSTRAINTS: &'static [ConstraintRef] = &[];
120    const CYCLE_SIZE: u64 = u64::MAX;
121}
122
123impl uor_foundation::pipeline::__sdk_seal::Sealed for JsonInput {}
124
125impl IntoBindingValue for JsonInput {
126    const MAX_BYTES: usize = JSON_INPUT_MAX_BYTES;
127    fn into_binding_bytes(&self, out: &mut [u8]) -> Result<usize, ShapeViolation> {
128        if self.bytes.len() > out.len() {
129            return Err(Self::LENGTH_VIOLATION);
130        }
131        out[..self.bytes.len()].copy_from_slice(&self.bytes);
132        Ok(self.bytes.len())
133    }
134}
135
136// ─── Output shape: AddressLabel ─────────────────────────────────────────
137//
138// The ψ-pipeline label. Site count = 71 — exactly the wire-format
139// `sha256:<64hex>` ASCII width. The terminal ψ_9 resolver
140// ([`crate::resolvers::AddressKInvariantResolver`]) emits a 71-byte
141// κ-label whose bytes ARE the wire-format content address by
142// construction.
143//
144// **Algebraic-closure encoded** (wiki ADR-024 / ADR-026, plus the
145// glossary entries "Closure (substrate)" / "Closure (prism)" in
146// `12-Glossary.md`): the framework's canonical completeness
147// criterion is χ(N(C)) = SITE_COUNT and β_k = 0 for k ≥ 1.
148// `AddressLabel` declares 71 disjoint `Site` constraints — one per
149// wire-format-label byte position. Each constraint pins exactly one
150// site; site supports are pairwise disjoint; the constraint nerve
151// N(C) is 71 isolated vertices with no higher simplices. Therefore:
152//
153//   β_0 = 71,    β_k = 0 for k ≥ 1
154//   χ(N(C)) = β_0 - β_1 + … = 71 = SITE_COUNT
155//
156// The wiki's iterative-resolution discipline converges in n - χ(N(C))
157// = 0 residual rank: at ψ_9 all 71 sites are pinned simultaneously by
158// the κ-derivation projecting the typed `JsonInput` through the
159// canonical hash axis. Sites 0..7 are template-pinned (the ASCII
160// "sha256:" prefix); sites 7..71 are κ-pinned (the 64 lowercase-hex
161// digits of the SHA-256 digest).
162output_shape! {
163    pub struct AddressLabel;
164    impl ConstrainedTypeShape for AddressLabel {
165        const IRI: &'static str = "https://uor.foundation/addr/AddressLabel";
166        const SITE_COUNT: usize = 71;
167        const CONSTRAINTS: &'static [ConstraintRef] = &[
168            // 71 disjoint Site constraints — one per wire-format
169            // address byte position.
170            ConstraintRef::Site { position: 0 }, ConstraintRef::Site { position: 1 },
171            ConstraintRef::Site { position: 2 }, ConstraintRef::Site { position: 3 },
172            ConstraintRef::Site { position: 4 }, ConstraintRef::Site { position: 5 },
173            ConstraintRef::Site { position: 6 }, ConstraintRef::Site { position: 7 },
174            ConstraintRef::Site { position: 8 }, ConstraintRef::Site { position: 9 },
175            ConstraintRef::Site { position: 10 }, ConstraintRef::Site { position: 11 },
176            ConstraintRef::Site { position: 12 }, ConstraintRef::Site { position: 13 },
177            ConstraintRef::Site { position: 14 }, ConstraintRef::Site { position: 15 },
178            ConstraintRef::Site { position: 16 }, ConstraintRef::Site { position: 17 },
179            ConstraintRef::Site { position: 18 }, ConstraintRef::Site { position: 19 },
180            ConstraintRef::Site { position: 20 }, ConstraintRef::Site { position: 21 },
181            ConstraintRef::Site { position: 22 }, ConstraintRef::Site { position: 23 },
182            ConstraintRef::Site { position: 24 }, ConstraintRef::Site { position: 25 },
183            ConstraintRef::Site { position: 26 }, ConstraintRef::Site { position: 27 },
184            ConstraintRef::Site { position: 28 }, ConstraintRef::Site { position: 29 },
185            ConstraintRef::Site { position: 30 }, ConstraintRef::Site { position: 31 },
186            ConstraintRef::Site { position: 32 }, ConstraintRef::Site { position: 33 },
187            ConstraintRef::Site { position: 34 }, ConstraintRef::Site { position: 35 },
188            ConstraintRef::Site { position: 36 }, ConstraintRef::Site { position: 37 },
189            ConstraintRef::Site { position: 38 }, ConstraintRef::Site { position: 39 },
190            ConstraintRef::Site { position: 40 }, ConstraintRef::Site { position: 41 },
191            ConstraintRef::Site { position: 42 }, ConstraintRef::Site { position: 43 },
192            ConstraintRef::Site { position: 44 }, ConstraintRef::Site { position: 45 },
193            ConstraintRef::Site { position: 46 }, ConstraintRef::Site { position: 47 },
194            ConstraintRef::Site { position: 48 }, ConstraintRef::Site { position: 49 },
195            ConstraintRef::Site { position: 50 }, ConstraintRef::Site { position: 51 },
196            ConstraintRef::Site { position: 52 }, ConstraintRef::Site { position: 53 },
197            ConstraintRef::Site { position: 54 }, ConstraintRef::Site { position: 55 },
198            ConstraintRef::Site { position: 56 }, ConstraintRef::Site { position: 57 },
199            ConstraintRef::Site { position: 58 }, ConstraintRef::Site { position: 59 },
200            ConstraintRef::Site { position: 60 }, ConstraintRef::Site { position: 61 },
201            ConstraintRef::Site { position: 62 }, ConstraintRef::Site { position: 63 },
202            ConstraintRef::Site { position: 64 }, ConstraintRef::Site { position: 65 },
203            ConstraintRef::Site { position: 66 }, ConstraintRef::Site { position: 67 },
204            ConstraintRef::Site { position: 68 }, ConstraintRef::Site { position: 69 },
205            ConstraintRef::Site { position: 70 },
206        ];
207    }
208}
209
210// ─── The PrismModel ─────────────────────────────────────────────────────
211
212prism_model! {
213    pub struct AddressModel;
214    pub struct AddressRoute;
215    impl PrismModel<
216        DefaultHostTypes,
217        AddrBounds,
218        Sha256Hasher,
219        AddressResolverTuple<Sha256Hasher>
220    > for AddressModel {
221        type Input = JsonInput;
222        type Output = AddressLabel;
223        type Route = AddressRoute;
224        fn route(input: Self::Input) -> Self::Output {
225            address_inference(input)
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn json_input_round_trips_canonical_bytes() {
236        let canonical = br#"{"foo":"bar"}"#.to_vec();
237        let input = JsonInput::new(canonical.clone()).expect("within bounds");
238        let mut out = [0u8; 4096];
239        let n = input.into_binding_bytes(&mut out).expect("buffer fits");
240        assert_eq!(n, canonical.len());
241        assert_eq!(&out[..n], canonical.as_slice());
242    }
243
244    #[test]
245    fn json_input_rejects_oversize() {
246        let oversize = vec![0u8; JSON_INPUT_MAX_BYTES + 1];
247        let err = JsonInput::new(oversize).expect_err("must reject oversize");
248        assert_eq!(err.shape_iri, JsonInput::LENGTH_VIOLATION.shape_iri);
249    }
250
251    #[test]
252    fn address_label_site_count_matches_wire_format_width() {
253        assert_eq!(
254            <AddressLabel as ConstrainedTypeShape>::SITE_COUNT,
255            ADDRESS_LABEL_BYTES
256        );
257    }
258
259    #[test]
260    fn address_label_carries_seventy_one_disjoint_site_constraints() {
261        let cs = <AddressLabel as ConstrainedTypeShape>::CONSTRAINTS;
262        assert_eq!(cs.len(), 71, "71 Site constraints (algebraic-closure)");
263        for c in cs {
264            assert!(matches!(c, ConstraintRef::Site { .. }));
265        }
266    }
267
268    #[test]
269    fn address_label_constraints_pin_every_wire_format_site() {
270        let cs = <AddressLabel as ConstrainedTypeShape>::CONSTRAINTS;
271        let positions: Vec<u32> = cs
272            .iter()
273            .filter_map(|c| match c {
274                ConstraintRef::Site { position } => Some(*position),
275                _ => None,
276            })
277            .collect();
278        assert_eq!(positions.len(), 71);
279        for (i, &p) in positions.iter().enumerate() {
280            assert_eq!(p, i as u32, "Site_{i} pins position {i}");
281        }
282    }
283}