Skip to main content

uor_addr_1/
resolvers.rs

1//! `uor-addr-1`'s `ResolverTuple` — the eight ψ-stage substitution-axis
2//! realizations for the JSON content-address typed feature hierarchy
3//! (wiki ADR-036).
4//!
5//! Each resolver realizes its named mathematical role over the typed
6//! feature hierarchy's structural geometry. The ψ-chain is a
7//! deterministic, parametric, structure-preserving transformation;
8//! each stage's emitted bytes describe its mathematical content for
9//! the constraint geometry declared on `AddressLabel`.
10//!
11//! ## The constraint geometry
12//!
13//! `AddressLabel::CONSTRAINTS` declares 71 disjoint
14//! `ConstraintRef::Site` instances — one per wire-format-label byte
15//! (wiki ADR-024 / ADR-026 algebraic closure). For this geometry,
16//! the ψ-chain's stage outputs are fully determined:
17//!
18//! | ψ-stage | Mathematical content |
19//! |---|---|
20//! | ψ_1 Nerve N(C)            | 71 isolated vertices, no higher simplices |
21//! | ψ_2 ChainComplex C_•      | C_0 = ℤ^71, C_k = 0 for k ≥ 1; all ∂ = 0 |
22//! | ψ_3 HomologyGroups H_k    | H_0 = ℤ^71, H_k = 0 for k ≥ 1 |
23//! | ψ_5 CochainComplex C^•    | C^0 = ℤ^71, C^k = 0 for k ≥ 1; all δ = 0 |
24//! | ψ_6 CohomologyGroups H^k  | H^0 = ℤ^71, H^k = 0 for k ≥ 1 |
25//! | ψ_7 PostnikovTower P_n    | P_n = the discrete 71-set for all n ≥ 0 |
26//! | ψ_8 HomotopyGroups π_k    | π_0 = the 71-element set, π_k = 0 for k ≥ 1 |
27//! | ψ_9 KInvariants κ_n       | trivial signature + the wire-format κ-label |
28//!
29//! ## Carrier layout
30//!
31//! Each non-terminal ψ-stage emits a structural carrier:
32//!
33//! ```text
34//! [0..4)               u32 BE: canonical_len (length of canonical JSON bytes; ≤ 4092)
35//! [4..4+canonical_len) canonical-form JCS+NFC JSON bytes (threaded forward)
36//! [carrier_max - 95..carrier_max - 87)  u64 BE: ψ-stage tag
37//! [carrier_max - 87..carrier_max - 83)  u32 BE: vertex_count = 71
38//! [carrier_max - 83..carrier_max - 79)  u32 BE: highest_nontrivial_dim = 0
39//! [carrier_max - 79..carrier_max - 75)  u32 BE: reserved = 0
40//! [carrier_max - 75..carrier_max - 4)   71 × u8: per-vertex Site positions
41//! ```
42//!
43//! Carriers are fixed-width 4096 bytes (= `AddrBounds::TERM_VALUE_MAX_BYTES`)
44//! so the catamorphism's per-fold-rule scratch buffer suffices. The
45//! variable-length canonical-form prefix lives at the front; the
46//! constant-geometry tail at the back.
47//!
48//! ## ψ_9 KInvariant — the terminal label
49//!
50//! ψ_9 consumes the ψ_8 HomotopyGroups carrier and emits exactly 71
51//! bytes — the wire-format `sha256:<64hex>`. The k-invariant signature
52//! itself is trivial for π_0-only spaces (no obstruction classes), so
53//! the structural content reduces to π_0's 71 generators. ψ_9's
54//! load-bearing computation is the **structural κ-derivation**: the
55//! typed canonical-form JSON bytes project via the canonical hash axis
56//! to a 32-byte content-address; the 7-byte ASCII prefix `"sha256:"`
57//! plus the 64 lowercase-hex digits of the digest pin all 71 sites
58//! simultaneously. One σ-projection per `forward()` — deterministic
59//! in the typed input, no enumeration, no search.
60//!
61//! ## Pure-prism commitment — wiki ADR-035 + ADR-046
62//!
63//! The verb body composes only ψ-Term variants (`Term::Nerve /
64//! PostnikovTower / HomotopyGroups / KInvariants`) per ADR-035's
65//! ψ-residuals discipline. Resolver bodies admit σ-residuals per
66//! ADR-046's resolver-body iterative-resolution discipline — that is
67//! the wiki-sanctioned layer where the canonical hash axis is
68//! consumed (see [`AddressKInvariantResolver`] below). The non-
69//! terminal resolvers do not enumerate; they thread the canonical-
70//! form bytes forward through the structural ψ-functor each realises.
71//! From outside, `forward()` is one structural inference per
72//! `JsonInput` — the ψ-pipeline maps typed canonical-form bytes to
73//! the κ-label by structural transformation, never by search.
74
75use core::marker::PhantomData;
76
77use uor_foundation::enforcement::{Hasher, ShapeViolation};
78use uor_foundation::pipeline::{
79    ChainComplexBytes, ChainComplexResolver, CochainComplexBytes, CochainComplexResolver,
80    CohomologyGroupResolver, ConstrainedTypeShape, ConstraintRef, HomologyGroupResolver,
81    HomotopyGroupResolver, HomotopyGroupsBytes, KInvariantResolver, NerveResolver,
82    PostnikovResolver, PostnikovTowerBytes, SimplicialComplexBytes,
83};
84use uor_foundation::{HostBounds, ViolationKind};
85use uor_foundation_sdk::resolver;
86
87use crate::model::{AddressLabel, ADDRESS_LABEL_BYTES, JSON_INPUT_MAX_BYTES};
88use crate::shapes::bounds::AddrBounds;
89
90// ─── Carrier layout constants ──────────────────────────────────────────
91
92/// Wire-format `sha256:<64hex>` byte width — the ψ_9 output width.
93const WIRE_FORMAT_ADDRESS_BYTES: usize = ADDRESS_LABEL_BYTES;
94
95/// Maximum canonical-form JSON byte width carried in a ψ-stage carrier.
96/// Bounded by `JsonInput::MAX_BYTES` minus the 4-byte length prefix and
97/// the geometry-tail header reserved at the back.
98const CARRIER_BYTES: usize = <AddrBounds as HostBounds>::TERM_VALUE_MAX_BYTES;
99
100/// Nerve-vertex count — matches `AddressLabel::SITE_COUNT`.
101const NERVE_VERTEX_COUNT: u32 = 71;
102/// Highest-nontrivial-dim for the discrete 71-vertex space.
103const NERVE_HIGHEST_DIM: u32 = 0;
104
105/// Geometry-tail layout at the back of every non-terminal carrier:
106/// `stage_tag(8) ‖ vertex_count(4) ‖ highest_dim(4) ‖ reserved(4) ‖
107///  vertex_positions(71)` = 91 bytes total.
108const GEOMETRY_TAIL_BYTES: usize = 8 + 4 + 4 + 4 + NERVE_VERTEX_COUNT as usize;
109
110/// Length prefix at the front: 4-byte u32 BE.
111const LENGTH_PREFIX_BYTES: usize = 4;
112
113/// Maximum canonical-form payload width that fits in the carrier.
114/// Used by the compile-time invariant below.
115#[allow(dead_code)]
116const CARRIER_PAYLOAD_MAX: usize = CARRIER_BYTES - LENGTH_PREFIX_BYTES - GEOMETRY_TAIL_BYTES;
117
118/// Compile-time invariant: the JsonInput cap must fit within the
119/// carrier-payload region.
120const _: () = {
121    assert!(
122        JSON_INPUT_MAX_BYTES <= CARRIER_PAYLOAD_MAX,
123        "JsonInput::MAX_BYTES must fit within CARRIER_BYTES - LENGTH_PREFIX_BYTES - GEOMETRY_TAIL_BYTES"
124    );
125};
126
127/// Offset of the geometry tail's start within a carrier.
128const OFFSET_GEOMETRY_TAIL: usize = CARRIER_BYTES - GEOMETRY_TAIL_BYTES;
129/// Offset of the stage_tag within the geometry tail.
130const OFFSET_STAGE_TAG: usize = OFFSET_GEOMETRY_TAIL;
131/// Offset of the vertex_count within the geometry tail.
132const OFFSET_VERTEX_COUNT: usize = OFFSET_STAGE_TAG + 8;
133/// Offset of the highest_dim within the geometry tail.
134const OFFSET_HIGHEST_DIM: usize = OFFSET_VERTEX_COUNT + 4;
135/// Offset of the reserved word within the geometry tail.
136const OFFSET_RESERVED: usize = OFFSET_HIGHEST_DIM + 4;
137/// Offset of the per-vertex Site positions within the geometry tail.
138#[allow(dead_code)]
139const OFFSET_VERTEX_POSITIONS: usize = OFFSET_RESERVED + 4;
140
141// ─── ψ-stage tags ──────────────────────────────────────────────────────
142
143const PSI_1_NERVE_TAG: u64 = 1;
144const PSI_2_CHAIN_COMPLEX_TAG: u64 = 2;
145const PSI_3_HOMOLOGY_GROUPS_TAG: u64 = 3;
146const PSI_5_COCHAIN_COMPLEX_TAG: u64 = 5;
147const PSI_6_COHOMOLOGY_GROUPS_TAG: u64 = 6;
148const PSI_7_POSTNIKOV_TOWER_TAG: u64 = 7;
149const PSI_8_HOMOTOPY_GROUPS_TAG: u64 = 8;
150
151// ─── ShapeViolation IRIs ───────────────────────────────────────────────
152
153const INPUT_LEN_VIOLATION: ShapeViolation = ShapeViolation {
154    shape_iri: "https://uor.foundation/addr/resolver/JsonInputBytes",
155    constraint_iri: "https://uor.foundation/addr/resolver/JsonInputBytes/maxBytes",
156    property_iri: "https://uor.foundation/addr/resolver/JsonInputBytes/byteCount",
157    expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
158    min_count: 0,
159    max_count: JSON_INPUT_MAX_BYTES as u32,
160    kind: ViolationKind::ValueCheck,
161};
162
163const CARRIER_INPUT_VIOLATION: ShapeViolation = ShapeViolation {
164    shape_iri: "https://uor.foundation/addr/resolver/CarrierInput",
165    constraint_iri: "https://uor.foundation/addr/resolver/CarrierInput/expectedCarrierBytes",
166    property_iri: "https://uor.foundation/addr/resolver/CarrierInput/byteCount",
167    expected_range: "http://www.w3.org/2001/XMLSchema#unsignedInt",
168    min_count: CARRIER_BYTES as u32,
169    max_count: CARRIER_BYTES as u32,
170    kind: ViolationKind::ValueCheck,
171};
172
173const CARRIER_BUFFER_VIOLATION: ShapeViolation = ShapeViolation {
174    shape_iri: "https://uor.foundation/addr/resolver/CarrierBuffer",
175    constraint_iri: "https://uor.foundation/addr/resolver/CarrierBuffer/minBytes",
176    property_iri: "https://uor.foundation/addr/resolver/CarrierBuffer/byteCount",
177    expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
178    min_count: CARRIER_BYTES as u32,
179    max_count: 0,
180    kind: ViolationKind::ValueCheck,
181};
182
183const ADDR_OUTPUT_VIOLATION: ShapeViolation = ShapeViolation {
184    shape_iri: "https://uor.foundation/addr/resolver/AddressLabelBuffer",
185    constraint_iri: "https://uor.foundation/addr/resolver/AddressLabelBuffer/minBytes",
186    property_iri: "https://uor.foundation/addr/resolver/AddressLabelBuffer/byteCount",
187    expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
188    min_count: WIRE_FORMAT_ADDRESS_BYTES as u32,
189    max_count: 0,
190    kind: ViolationKind::ValueCheck,
191};
192
193const UPSTREAM_TAG_VIOLATION: ShapeViolation = ShapeViolation {
194    shape_iri: "https://uor.foundation/addr/resolver/UpstreamStageTag",
195    constraint_iri: "https://uor.foundation/addr/resolver/UpstreamStageTag/expectedTag",
196    property_iri: "https://uor.foundation/addr/resolver/UpstreamStageTag/stageTag",
197    expected_range: "http://www.w3.org/2001/XMLSchema#unsignedLong",
198    min_count: 1,
199    max_count: 1,
200    kind: ViolationKind::ValueCheck,
201};
202
203const GEOMETRY_VIOLATION: ShapeViolation = ShapeViolation {
204    shape_iri: "https://uor.foundation/addr/resolver/CarrierGeometry",
205    constraint_iri:
206        "https://uor.foundation/addr/resolver/CarrierGeometry/seventyOneVerticesDiscrete",
207    property_iri: "https://uor.foundation/addr/resolver/CarrierGeometry/vertexCount",
208    expected_range: "http://www.w3.org/2001/XMLSchema#unsignedInt",
209    min_count: NERVE_VERTEX_COUNT,
210    max_count: NERVE_VERTEX_COUNT,
211    kind: ViolationKind::ValueCheck,
212};
213
214// ─── Compile-time validation of AddressLabel::CONSTRAINTS ──────────────
215
216// The 71-disjoint-Site algebraic-closure shape is a compile-time
217// invariant of `AddressLabel`. Any malformed CONSTRAINTS declaration
218// fails the build before reaching ψ_1.
219const _: () = {
220    let cs = <AddressLabel as ConstrainedTypeShape>::CONSTRAINTS;
221    assert!(
222        cs.len() == NERVE_VERTEX_COUNT as usize,
223        "AddressLabel::CONSTRAINTS must declare exactly 71 Site instances (algebraic-closure)"
224    );
225    let mut i = 0;
226    while i < cs.len() {
227        match cs[i] {
228            ConstraintRef::Site { position } => {
229                assert!(
230                    position as usize == i,
231                    "AddressLabel::CONSTRAINTS: Site_i must pin position i for i ∈ [0, 71)"
232                );
233            }
234            _ => panic!("AddressLabel::CONSTRAINTS may only contain ConstraintRef::Site instances"),
235        }
236        i += 1;
237    }
238};
239
240// ─── Const geometry-tail template ──────────────────────────────────────
241
242/// The constant geometry-tail bytes minus the stage_tag (which varies
243/// per stage). Layout within the slice:
244/// `vertex_count(4) ‖ highest_dim(4) ‖ reserved(4) ‖ vertex_positions(71)`
245/// = 83 bytes. Lifted to a `const` so emitting a carrier is bit-copy.
246const GEOMETRY_TAIL_AFTER_STAGE_TAG: [u8; GEOMETRY_TAIL_BYTES - 8] = {
247    let mut t = [0u8; GEOMETRY_TAIL_BYTES - 8];
248    let vc = NERVE_VERTEX_COUNT.to_be_bytes();
249    t[0] = vc[0];
250    t[1] = vc[1];
251    t[2] = vc[2];
252    t[3] = vc[3];
253    // highest_dim @ [4..8) and reserved @ [8..12) are zero-init.
254    let mut i = 0;
255    while i < NERVE_VERTEX_COUNT as usize {
256        t[12 + i] = i as u8;
257        i += 1;
258    }
259    t
260};
261
262// ─── Carrier encode/decode ─────────────────────────────────────────────
263
264/// Decoded view of a structural ψ-stage carrier.
265struct DecodedCarrier<'a> {
266    /// Canonical-form JCS+NFC JSON bytes (length carried in
267    /// `canonical_len`; live within `[LENGTH_PREFIX_BYTES,
268    /// LENGTH_PREFIX_BYTES + canonical_len)` of the original buffer).
269    canonical: &'a [u8],
270    /// Upstream ψ-stage tag.
271    stage_tag: u64,
272    /// Nerve vertex count — invariant 71.
273    vertex_count: u32,
274    /// Highest nontrivial dimension — invariant 0.
275    highest_dim: u32,
276}
277
278#[inline]
279fn decode_carrier(bytes: &[u8]) -> Result<DecodedCarrier<'_>, ShapeViolation> {
280    if bytes.len() != CARRIER_BYTES {
281        return Err(CARRIER_INPUT_VIOLATION);
282    }
283    let canonical_len =
284        u32::from_be_bytes(bytes[..LENGTH_PREFIX_BYTES].try_into().unwrap_or([0; 4])) as usize;
285    if canonical_len > JSON_INPUT_MAX_BYTES {
286        return Err(INPUT_LEN_VIOLATION);
287    }
288    let canonical = &bytes[LENGTH_PREFIX_BYTES..LENGTH_PREFIX_BYTES + canonical_len];
289    let stage_tag = u64::from_be_bytes(
290        bytes[OFFSET_STAGE_TAG..OFFSET_VERTEX_COUNT]
291            .try_into()
292            .unwrap_or([0; 8]),
293    );
294    let vertex_count = u32::from_be_bytes(
295        bytes[OFFSET_VERTEX_COUNT..OFFSET_HIGHEST_DIM]
296            .try_into()
297            .unwrap_or([0; 4]),
298    );
299    let highest_dim = u32::from_be_bytes(
300        bytes[OFFSET_HIGHEST_DIM..OFFSET_RESERVED]
301            .try_into()
302            .unwrap_or([0; 4]),
303    );
304    Ok(DecodedCarrier {
305        canonical,
306        stage_tag,
307        vertex_count,
308        highest_dim,
309    })
310}
311
312/// Validate the upstream carrier: stage tag matches what this
313/// downstream ψ-functor expects; structural geometry is the
314/// 71-isolated-vertices algebraic-closure shape `AddressLabel::CONSTRAINTS`
315/// declares.
316#[inline]
317fn validate_upstream(
318    carrier: &DecodedCarrier<'_>,
319    expected_upstream_tag: u64,
320) -> Result<(), ShapeViolation> {
321    if carrier.stage_tag != expected_upstream_tag {
322        return Err(UPSTREAM_TAG_VIOLATION);
323    }
324    if carrier.vertex_count != NERVE_VERTEX_COUNT {
325        return Err(GEOMETRY_VIOLATION);
326    }
327    if carrier.highest_dim != NERVE_HIGHEST_DIM {
328        return Err(GEOMETRY_VIOLATION);
329    }
330    Ok(())
331}
332
333/// Emit a structural carrier with the given stage tag. The canonical-
334/// form JSON bytes preserve the typed `JsonInput` data; the geometry
335/// data (vertex_count = 71, highest_dim = 0, positions = [0..71))
336/// lives in [`GEOMETRY_TAIL_AFTER_STAGE_TAG`] as a compile-time
337/// constant and is bit-copied into the carrier as-is.
338#[inline]
339fn emit_carrier(canonical: &[u8], stage_tag: u64, out: &mut [u8]) -> Result<usize, ShapeViolation> {
340    if out.len() < CARRIER_BYTES {
341        return Err(CARRIER_BUFFER_VIOLATION);
342    }
343    if canonical.len() > JSON_INPUT_MAX_BYTES {
344        return Err(INPUT_LEN_VIOLATION);
345    }
346
347    // Length prefix.
348    let len_bytes = (canonical.len() as u32).to_be_bytes();
349    out[..LENGTH_PREFIX_BYTES].copy_from_slice(&len_bytes);
350    // Canonical-form bytes.
351    out[LENGTH_PREFIX_BYTES..LENGTH_PREFIX_BYTES + canonical.len()].copy_from_slice(canonical);
352    // Zero-pad between canonical region and geometry tail.
353    let zero_start = LENGTH_PREFIX_BYTES + canonical.len();
354    out[zero_start..OFFSET_GEOMETRY_TAIL].fill(0);
355    // Stage tag (8 bytes, big-endian).
356    out[OFFSET_STAGE_TAG..OFFSET_VERTEX_COUNT].copy_from_slice(&stage_tag.to_be_bytes());
357    // Geometry tail (vertex_count, highest_dim, reserved, vertex_positions).
358    out[OFFSET_VERTEX_COUNT..].copy_from_slice(&GEOMETRY_TAIL_AFTER_STAGE_TAG);
359
360    Ok(CARRIER_BYTES)
361}
362
363// ─── ψ_1 Nerve — Constraints → SimplicialComplex ───────────────────────
364
365/// ψ_1 `Nerve` resolver — `Constraints → SimplicialComplex`.
366///
367/// Builds N(C) for `AddressLabel::CONSTRAINTS`. For uor-addr-1's 71
368/// disjoint `ConstraintRef::Site` declarations, N(C) is 71 isolated
369/// vertices with no higher simplices.
370#[derive(Debug)]
371pub struct AddressNerveResolver<H>(PhantomData<H>);
372
373impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressNerveResolver<H> {}
374
375impl<H: Hasher> NerveResolver<H> for AddressNerveResolver<H> {
376    #[inline]
377    fn resolve(&self, input: &[u8], out: &mut [u8]) -> Result<usize, ShapeViolation> {
378        if input.len() > JSON_INPUT_MAX_BYTES {
379            return Err(INPUT_LEN_VIOLATION);
380        }
381        emit_carrier(input, PSI_1_NERVE_TAG, out)
382    }
383}
384
385// ─── ψ_2 ChainComplex — SimplicialComplex → ChainComplex ───────────────
386
387/// ψ_2 `ChainComplex` resolver — `SimplicialComplex → ChainComplex`.
388/// Off-path on the address-derivation transform.
389#[derive(Debug)]
390pub struct AddressChainComplexResolver<H>(PhantomData<H>);
391
392impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressChainComplexResolver<H> {}
393
394impl<H: Hasher> ChainComplexResolver<H> for AddressChainComplexResolver<H> {
395    #[inline]
396    fn resolve(
397        &self,
398        input: SimplicialComplexBytes<'_>,
399        out: &mut [u8],
400    ) -> Result<usize, ShapeViolation> {
401        let carrier = decode_carrier(input.as_bytes())?;
402        validate_upstream(&carrier, PSI_1_NERVE_TAG)?;
403        emit_carrier(carrier.canonical, PSI_2_CHAIN_COMPLEX_TAG, out)
404    }
405}
406
407// ─── ψ_3 HomologyGroups — ChainComplex → HomologyGroups ────────────────
408
409/// ψ_3 `HomologyGroups` resolver. Off-path.
410#[derive(Debug)]
411pub struct AddressHomologyGroupResolver<H>(PhantomData<H>);
412
413impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressHomologyGroupResolver<H> {}
414
415impl<H: Hasher> HomologyGroupResolver<H> for AddressHomologyGroupResolver<H> {
416    #[inline]
417    fn resolve(
418        &self,
419        input: ChainComplexBytes<'_>,
420        out: &mut [u8],
421    ) -> Result<usize, ShapeViolation> {
422        let carrier = decode_carrier(input.as_bytes())?;
423        validate_upstream(&carrier, PSI_2_CHAIN_COMPLEX_TAG)?;
424        emit_carrier(carrier.canonical, PSI_3_HOMOLOGY_GROUPS_TAG, out)
425    }
426}
427
428// ─── ψ_5 CochainComplex — ChainComplex → CochainComplex ────────────────
429
430/// ψ_5 `CochainComplex` resolver. Off-path.
431#[derive(Debug)]
432pub struct AddressCochainComplexResolver<H>(PhantomData<H>);
433
434impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressCochainComplexResolver<H> {}
435
436impl<H: Hasher> CochainComplexResolver<H> for AddressCochainComplexResolver<H> {
437    #[inline]
438    fn resolve(
439        &self,
440        input: ChainComplexBytes<'_>,
441        out: &mut [u8],
442    ) -> Result<usize, ShapeViolation> {
443        let carrier = decode_carrier(input.as_bytes())?;
444        validate_upstream(&carrier, PSI_2_CHAIN_COMPLEX_TAG)?;
445        emit_carrier(carrier.canonical, PSI_5_COCHAIN_COMPLEX_TAG, out)
446    }
447}
448
449// ─── ψ_6 CohomologyGroups — CochainComplex → CohomologyGroups ──────────
450
451/// ψ_6 `CohomologyGroups` resolver. Off-path.
452#[derive(Debug)]
453pub struct AddressCohomologyGroupResolver<H>(PhantomData<H>);
454
455impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressCohomologyGroupResolver<H> {}
456
457impl<H: Hasher> CohomologyGroupResolver<H> for AddressCohomologyGroupResolver<H> {
458    #[inline]
459    fn resolve(
460        &self,
461        input: CochainComplexBytes<'_>,
462        out: &mut [u8],
463    ) -> Result<usize, ShapeViolation> {
464        let carrier = decode_carrier(input.as_bytes())?;
465        validate_upstream(&carrier, PSI_5_COCHAIN_COMPLEX_TAG)?;
466        emit_carrier(carrier.canonical, PSI_6_COHOMOLOGY_GROUPS_TAG, out)
467    }
468}
469
470// ─── ψ_7 PostnikovTower — SimplicialComplex → PostnikovTower ───────────
471
472/// ψ_7 `PostnikovTower` resolver. On the address-derivation transform
473/// (ψ_1 → ψ_7 → ψ_8 → ψ_9).
474#[derive(Debug)]
475pub struct AddressPostnikovResolver<H>(PhantomData<H>);
476
477impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressPostnikovResolver<H> {}
478
479impl<H: Hasher> PostnikovResolver<H> for AddressPostnikovResolver<H> {
480    #[inline]
481    fn resolve(
482        &self,
483        input: SimplicialComplexBytes<'_>,
484        out: &mut [u8],
485    ) -> Result<usize, ShapeViolation> {
486        let carrier = decode_carrier(input.as_bytes())?;
487        validate_upstream(&carrier, PSI_1_NERVE_TAG)?;
488        emit_carrier(carrier.canonical, PSI_7_POSTNIKOV_TOWER_TAG, out)
489    }
490}
491
492// ─── ψ_8 HomotopyGroups — PostnikovTower → HomotopyGroups ──────────────
493
494/// ψ_8 `HomotopyGroups` resolver. On the address-derivation transform.
495#[derive(Debug)]
496pub struct AddressHomotopyGroupResolver<H>(PhantomData<H>);
497
498impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressHomotopyGroupResolver<H> {}
499
500impl<H: Hasher> HomotopyGroupResolver<H> for AddressHomotopyGroupResolver<H> {
501    #[inline]
502    fn resolve(
503        &self,
504        input: PostnikovTowerBytes<'_>,
505        out: &mut [u8],
506    ) -> Result<usize, ShapeViolation> {
507        let carrier = decode_carrier(input.as_bytes())?;
508        validate_upstream(&carrier, PSI_7_POSTNIKOV_TOWER_TAG)?;
509        emit_carrier(carrier.canonical, PSI_8_HOMOTOPY_GROUPS_TAG, out)
510    }
511}
512
513// ─── ψ_9 KInvariants — the terminal label ──────────────────────────────
514
515/// ψ_9 `KInvariant` resolver — the terminal ψ-stage that materialises
516/// the 71-byte wire-format content address.
517///
518/// **The κ-derivation.** ψ_9 projects the typed canonical-form bytes
519/// through the canonical hash axis (`H: Hasher` — the substitution-axis
520/// selection per ADR-030):
521///
522/// ```text
523/// digest = H::initial().fold_bytes(canonical_bytes).finalize()
524/// label  = b"sha256:" ‖ hex_lower(digest)
525/// ```
526///
527/// One σ-projection total per `forward()` — deterministic in the typed
528/// input, no enumeration. The wiki's iterative-resolution discipline
529/// converges here: all 71 free sites pin simultaneously to the
530/// κ-derivation; `FreeRank` over `AddressLabel` drops from 71 to 0 in
531/// this single terminal stage.
532///
533/// This is the ONE place in the crate where the canonical hash axis is
534/// consumed. The verb body's term composition never invokes the axis
535/// (no `Term::AxisInvocation`); the axis flows as a type parameter
536/// only, reaching the resolver body through the model's substrate-axis
537/// selection (`Sha256Hasher` per [`crate::pipeline::address`]).
538#[derive(Debug)]
539pub struct AddressKInvariantResolver<H>(PhantomData<H>);
540
541impl<H: Hasher> uor_foundation::pipeline::__sdk_seal::Sealed for AddressKInvariantResolver<H> {}
542
543impl<H: Hasher> KInvariantResolver<H> for AddressKInvariantResolver<H> {
544    #[inline]
545    fn resolve(
546        &self,
547        input: HomotopyGroupsBytes<'_>,
548        out: &mut [u8],
549    ) -> Result<usize, ShapeViolation> {
550        if out.len() < WIRE_FORMAT_ADDRESS_BYTES {
551            return Err(ADDR_OUTPUT_VIOLATION);
552        }
553
554        let carrier = decode_carrier(input.as_bytes())?;
555        validate_upstream(&carrier, PSI_8_HOMOTOPY_GROUPS_TAG)?;
556
557        // Structural κ-derivation: project the typed canonical-form
558        // bytes via the canonical hash axis to a 32-byte digest. One
559        // σ-projection — no enumeration.
560        let digest: [u8; 32] = H::initial().fold_bytes(carrier.canonical).finalize_to_32();
561
562        // Emit `"sha256:"` (7 bytes) + 64-lowercase-hex digest = 71 bytes.
563        out[..7].copy_from_slice(b"sha256:");
564        for (i, byte) in digest.iter().enumerate() {
565            out[7 + 2 * i] = HEX_LOWER[(byte >> 4) as usize];
566            out[7 + 2 * i + 1] = HEX_LOWER[(byte & 0x0F) as usize];
567        }
568
569        Ok(WIRE_FORMAT_ADDRESS_BYTES)
570    }
571}
572
573const HEX_LOWER: [u8; 16] = *b"0123456789abcdef";
574
575/// Extension trait to take a `Hasher::finalize()` output of arbitrary
576/// `FP_MAX` and view its first 32 bytes (the SHA-256 active width per
577/// `Sha256Hasher::OUTPUT_BYTES = 32`). Foundation's `Hasher::finalize`
578/// returns `[u8; FP_MAX]` for the const-generic `FP_MAX`; this helper
579/// names the W32 slot the κ-derivation reads.
580trait FinalizeTo32 {
581    fn finalize_to_32(self) -> [u8; 32];
582}
583
584impl<H: Hasher> FinalizeTo32 for H {
585    #[inline]
586    fn finalize_to_32(self) -> [u8; 32] {
587        let buf = <H as Hasher>::finalize(self);
588        let mut out = [0u8; 32];
589        // The active width is `H::OUTPUT_BYTES` (= 32 for Sha256Hasher);
590        // foundation guarantees bytes [OUTPUT_BYTES..FP_MAX] are zero,
591        // so this is a straight copy of the first 32 bytes.
592        out.copy_from_slice(&buf[..32]);
593        out
594    }
595}
596
597// ─── Default impls + ResolverTuple ─────────────────────────────────────
598
599macro_rules! impl_default_for_resolver {
600    ($resolver:ident) => {
601        impl<H: Hasher> Default for $resolver<H> {
602            #[inline]
603            fn default() -> Self {
604                Self(PhantomData)
605            }
606        }
607    };
608}
609
610impl_default_for_resolver!(AddressNerveResolver);
611impl_default_for_resolver!(AddressChainComplexResolver);
612impl_default_for_resolver!(AddressHomologyGroupResolver);
613impl_default_for_resolver!(AddressCochainComplexResolver);
614impl_default_for_resolver!(AddressCohomologyGroupResolver);
615impl_default_for_resolver!(AddressPostnikovResolver);
616impl_default_for_resolver!(AddressHomotopyGroupResolver);
617impl_default_for_resolver!(AddressKInvariantResolver);
618
619resolver! {
620    pub struct AddressResolverTuple<H: ::uor_foundation::enforcement::Hasher> {
621        nerve: AddressNerveResolver<H>,
622        chain_complex: AddressChainComplexResolver<H>,
623        homology_groups: AddressHomologyGroupResolver<H>,
624        cochain_complex: AddressCochainComplexResolver<H>,
625        cohomology_groups: AddressCohomologyGroupResolver<H>,
626        postnikov: AddressPostnikovResolver<H>,
627        homotopy_groups: AddressHomotopyGroupResolver<H>,
628        k_invariants: AddressKInvariantResolver<H>,
629    }
630}
631
632// ─── Compile-time invariants ───────────────────────────────────────────
633
634const _: () = {
635    assert!(
636        CARRIER_BYTES <= <AddrBounds as HostBounds>::NERVE_OUTPUT_BYTES_MAX,
637        "ψ_1 carrier exceeds NERVE_OUTPUT_BYTES_MAX"
638    );
639    assert!(
640        CARRIER_BYTES <= <AddrBounds as HostBounds>::CHAIN_COMPLEX_OUTPUT_BYTES_MAX,
641        "ψ_2 carrier exceeds CHAIN_COMPLEX_OUTPUT_BYTES_MAX"
642    );
643    assert!(
644        CARRIER_BYTES <= <AddrBounds as HostBounds>::HOMOLOGY_GROUPS_OUTPUT_BYTES_MAX,
645        "ψ_3 carrier exceeds HOMOLOGY_GROUPS_OUTPUT_BYTES_MAX"
646    );
647    assert!(
648        CARRIER_BYTES <= <AddrBounds as HostBounds>::COCHAIN_COMPLEX_OUTPUT_BYTES_MAX,
649        "ψ_5 carrier exceeds COCHAIN_COMPLEX_OUTPUT_BYTES_MAX"
650    );
651    assert!(
652        CARRIER_BYTES <= <AddrBounds as HostBounds>::COHOMOLOGY_GROUPS_OUTPUT_BYTES_MAX,
653        "ψ_6 carrier exceeds COHOMOLOGY_GROUPS_OUTPUT_BYTES_MAX"
654    );
655    assert!(
656        CARRIER_BYTES <= <AddrBounds as HostBounds>::POSTNIKOV_TOWER_OUTPUT_BYTES_MAX,
657        "ψ_7 carrier exceeds POSTNIKOV_TOWER_OUTPUT_BYTES_MAX"
658    );
659    assert!(
660        CARRIER_BYTES <= <AddrBounds as HostBounds>::HOMOTOPY_GROUPS_OUTPUT_BYTES_MAX,
661        "ψ_8 carrier exceeds HOMOTOPY_GROUPS_OUTPUT_BYTES_MAX"
662    );
663    assert!(
664        WIRE_FORMAT_ADDRESS_BYTES <= <AddrBounds as HostBounds>::K_INVARIANTS_OUTPUT_BYTES_MAX,
665        "ψ_9 label exceeds K_INVARIANTS_OUTPUT_BYTES_MAX"
666    );
667};