Skip to main content

u280_hardware_promoter/
route.rs

1//! Typed policy boundary for a future exact accepted-route parser.
2//!
3//! This module deliberately defines no AMD report or xclbin wire format. A
4//! future descriptor-retaining parser must derive [`RoutePolicyInputs`] from the
5//! immutable files already named by [`PromotionRequest`]. The metadata policy
6//! validator still cannot construct [`AcceptedRouteEvidence`]; only a future
7//! parser-owned retained-descriptor guard can make that type inhabitable. The
8//! current public entry point remains fail-closed while the tracked Vitis
9//! format profile is `adaptation-required`.
10
11use crate::canonical::{hex_lower, is_lower_hex};
12use crate::model::{
13    BASELINE_CLOCK_HZ, MAXIMUM_DSPS, MAXIMUM_LUTS, MAXIMUM_REGISTERS, PERFORMANCE_PROFILE,
14    PromotionRequest, RequestFileLocator, validate_absolute_path,
15};
16use crate::{FailureCode, PromotionError, PromotionResult};
17use sha2::{Digest as _, Sha256};
18use std::collections::BTreeSet;
19
20const ROUTE_ADAPTATION_STATUS: &str = "adaptation-required";
21const ACCEPTANCE_EVIDENCE_TIER: &str = "vitis-link-route";
22const ACCEPTANCE_CAPTURE_KIND: &str = "acceptance-link";
23const ACCEPTANCE_STATUS: &str = "pass";
24const REQUIRED_AMD_RELEASE: &str = "2023.2";
25const REQUIRED_PLATFORM: &str = "xilinx_u280_gen3x16_xdma_1_202211_1";
26const REQUIRED_DEVICE_PART: &str = "xcu280-fsvh2892-2L-e";
27const REQUIRED_XCLBIN_TARGET: &str = "hw";
28const REPORT_SET_DIGEST_DOMAIN: &[u8] = b"hashsigs-u280-route-report-set-v1\0";
29const XCLBIN_IDENTITY_DIGEST_DOMAIN: &[u8] = b"hashsigs-u280-route-xclbin-identity-v1\0";
30// Matches `FILE_BOUNDARY_LIMITS.maxRunFiles` in the stable Vitis contract. A
31// future parser must reject a larger descriptor-derived report set before
32// reaching this model.
33const MAXIMUM_RAW_ROUTE_REPORTS: usize = 100_000;
34
35/// One opaque accepted 250 MHz route-policy proof.
36///
37/// The type has no public constructor, serde implementation, or `Clone`
38/// implementation. Its private retained-descriptor guard is uninhabited in
39/// this source slice, so even module-private metadata policy validation cannot
40/// construct it. A future exact parser must install the descriptor guard and
41/// revalidate it before this type can become inhabited.
42///
43/// ```compile_fail
44/// use u280_hardware_promoter::AcceptedRouteEvidence;
45///
46/// fn assert_clone<T: Clone>() {}
47/// assert_clone::<AcceptedRouteEvidence>();
48/// ```
49///
50/// ```compile_fail
51/// use serde::de::DeserializeOwned;
52/// use u280_hardware_promoter::AcceptedRouteEvidence;
53///
54/// fn assert_deserializable<T: DeserializeOwned>() {}
55/// assert_deserializable::<AcceptedRouteEvidence>();
56/// ```
57///
58/// ```compile_fail
59/// use u280_hardware_promoter::AcceptedRouteEvidence;
60///
61/// fn external_representation_is_private(value: AcceptedRouteEvidence) {
62///     let AcceptedRouteEvidence {
63///         policy: _,
64///         retained_descriptors: _,
65///     } = value;
66/// }
67/// ```
68#[derive(Debug)]
69pub struct AcceptedRouteEvidence {
70    policy: ValidatedRoutePolicy,
71    #[allow(dead_code)] // Replaced by the future parser-owned descriptor guard.
72    retained_descriptors: RetainedRouteDescriptorGuard,
73}
74
75/// Metadata policy output that is deliberately insufficient for acceptance.
76#[derive(Debug)]
77struct ValidatedRoutePolicy {
78    design_digest: String,
79    actual_routed_hz: u64,
80    setup_wns_femtoseconds: i64,
81    hold_whs_femtoseconds: i64,
82    whole_design_luts: u64,
83    whole_design_registers: u64,
84    whole_design_dsps: u64,
85    stage_manifest: RouteArtifactBinding,
86    run_inventory: RouteArtifactBinding,
87    route_seal: RouteArtifactBinding,
88    raw_reports: Vec<RouteArtifactBinding>,
89    routed_clock_evidence: RouteArtifactBinding,
90    xclbin_identity: RouteXclbinIdentity,
91}
92
93/// Placeholder preventing metadata-only policy output from becoming evidence.
94#[derive(Debug)]
95enum RetainedRouteDescriptorGuard {}
96
97impl AcceptedRouteEvidence {
98    /// Design-family digest joining the accepted 250 MHz stage and route.
99    #[must_use]
100    pub fn design_digest(&self) -> &str {
101        &self.policy.design_digest
102    }
103
104    /// Actual routed clock accepted from the exact 250 MHz route proof.
105    #[must_use]
106    pub const fn actual_routed_hz(&self) -> u64 {
107        self.policy.actual_routed_hz
108    }
109
110    /// Exact accepted setup worst negative slack, represented in femtoseconds.
111    #[must_use]
112    pub const fn setup_wns_femtoseconds(&self) -> i64 {
113        self.policy.setup_wns_femtoseconds
114    }
115
116    /// Exact accepted hold worst hold slack, represented in femtoseconds.
117    #[must_use]
118    pub const fn hold_whs_femtoseconds(&self) -> i64 {
119        self.policy.hold_whs_femtoseconds
120    }
121
122    /// Whole-routed-design LUT count, including shell resources.
123    #[must_use]
124    pub const fn whole_design_luts(&self) -> u64 {
125        self.policy.whole_design_luts
126    }
127
128    /// Whole-routed-design register count, including shell resources.
129    #[must_use]
130    pub const fn whole_design_registers(&self) -> u64 {
131        self.policy.whole_design_registers
132    }
133
134    /// Whole-routed-design DSP count, including shell resources.
135    #[must_use]
136    pub const fn whole_design_dsps(&self) -> u64 {
137        self.policy.whole_design_dsps
138    }
139
140    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
141    pub(crate) const fn stage_manifest(&self) -> &RouteArtifactBinding {
142        &self.policy.stage_manifest
143    }
144
145    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
146    pub(crate) const fn run_inventory(&self) -> &RouteArtifactBinding {
147        &self.policy.run_inventory
148    }
149
150    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
151    pub(crate) const fn route_seal(&self) -> &RouteArtifactBinding {
152        &self.policy.route_seal
153    }
154
155    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
156    pub(crate) fn raw_reports(&self) -> &[RouteArtifactBinding] {
157        &self.policy.raw_reports
158    }
159
160    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
161    pub(crate) const fn routed_clock_evidence(&self) -> &RouteArtifactBinding {
162        &self.policy.routed_clock_evidence
163    }
164
165    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
166    pub(crate) const fn xclbin(&self) -> &RouteArtifactBinding {
167        &self.policy.xclbin_identity.subject
168    }
169
170    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
171    pub(crate) const fn xclbin_identity(&self) -> &RouteXclbinIdentity {
172        &self.policy.xclbin_identity
173    }
174}
175
176/// Fails closed until an immutable format probe has produced an exact parser
177/// contract and a later clean commit installs that parser.
178///
179/// The typed policy model in this module is not a parser and cannot make the
180/// current format-probe capture promotable.
181///
182/// # Errors
183///
184/// Always returns [`FailureCode::RouteUnaccepted`] in this source slice.
185pub fn require_accepted_route(
186    _request: &PromotionRequest,
187) -> PromotionResult<AcceptedRouteEvidence> {
188    Err(PromotionError::new(
189        FailureCode::RouteUnaccepted,
190        format!(
191            "Vitis route evidence status is {ROUTE_ADAPTATION_STATUS}; exact accepted-route parser is unavailable"
192        ),
193    ))
194}
195
196/// Exact artifact subject derived by the future descriptor-retaining parser.
197///
198/// The parser must derive the byte identity and physical subject from one
199/// retained regular-file descriptor. This type is crate-private and is not
200/// deserializable; caller-supplied path or digest metadata cannot construct it.
201#[derive(Debug, Eq, PartialEq)]
202pub(crate) struct RouteArtifactBinding {
203    absolute_path: String,
204    bytes: u64,
205    sha256: String,
206    descriptor: ParserDescriptorSubject,
207}
208
209/// Physical identity sampled from the retained parser-owned descriptor.
210#[derive(Debug, Eq, PartialEq)]
211struct ParserDescriptorSubject {
212    canonical_realpath: String,
213    device: u64,
214    inode: u64,
215}
216
217impl RouteArtifactBinding {
218    #[allow(dead_code)] // Future exact parser output constructor.
219    fn from_exact_parser(
220        absolute_path: String,
221        canonical_realpath: String,
222        bytes: u64,
223        sha256: String,
224        device: u64,
225        inode: u64,
226    ) -> Self {
227        Self {
228            absolute_path,
229            bytes,
230            sha256,
231            descriptor: ParserDescriptorSubject {
232                canonical_realpath,
233                device,
234                inode,
235            },
236        }
237    }
238
239    pub(crate) fn absolute_path(&self) -> &str {
240        &self.absolute_path
241    }
242
243    #[allow(dead_code)] // Consumed by the future retained-descriptor guard.
244    pub(crate) fn canonical_realpath(&self) -> &str {
245        &self.descriptor.canonical_realpath
246    }
247
248    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
249    pub(crate) const fn bytes(&self) -> u64 {
250        self.bytes
251    }
252
253    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
254    pub(crate) fn sha256(&self) -> &str {
255        &self.sha256
256    }
257
258    #[allow(dead_code)] // Consumed by the future retained-descriptor guard.
259    pub(crate) const fn device(&self) -> u64 {
260        self.descriptor.device
261    }
262
263    #[allow(dead_code)] // Consumed by the future retained-descriptor guard.
264    pub(crate) const fn inode(&self) -> u64 {
265        self.descriptor.inode
266    }
267
268    fn validate(&self, label: &str) -> PromotionResult<()> {
269        validate_absolute_path(&self.absolute_path, label)?;
270        validate_absolute_path(&self.descriptor.canonical_realpath, label)?;
271        if self.absolute_path != self.descriptor.canonical_realpath {
272            return Err(PromotionError::new(
273                FailureCode::IdentityJoin,
274                format!("{label} pathname differs from its descriptor-derived canonical realpath"),
275            ));
276        }
277        if self.descriptor.inode == 0 {
278            return Err(PromotionError::new(
279                FailureCode::Noncanonical,
280                format!("{label} has an invalid zero descriptor inode"),
281            ));
282        }
283        if self.bytes == 0 || !is_lower_hex(&self.sha256, 64, true) {
284            return Err(PromotionError::new(
285                FailureCode::Noncanonical,
286                format!("{label} has a zero length or non-canonical SHA-256"),
287            ));
288        }
289        Ok(())
290    }
291
292    fn require_request_binding(
293        &self,
294        request: &RequestFileLocator<'_>,
295        label: &str,
296    ) -> PromotionResult<()> {
297        if self.absolute_path != request.absolute_path()
298            || self.bytes != request.bytes()
299            || self.sha256 != request.sha256()
300        {
301            return Err(PromotionError::new(
302                FailureCode::IdentityJoin,
303                format!("{label} differs from the exact promotion-request binding"),
304            ));
305        }
306        Ok(())
307    }
308}
309
310/// Route-derived xclbin identity retained for the later exact card join.
311///
312/// This type has no constructor outside the route parser module, no serde
313/// implementation, and no `Clone` implementation. It deliberately contains no
314/// physical-card fields. Its internal transcript joins every identity field to
315/// one exact descriptor-derived xclbin subject.
316#[derive(Debug)]
317pub(crate) struct RouteXclbinIdentity {
318    subject: RouteArtifactBinding,
319    uuid: String,
320    interface_uuid: String,
321    deployment_platform: String,
322    fpga_part: String,
323    target: String,
324    descriptor_identity_sha256: String,
325}
326
327impl RouteXclbinIdentity {
328    #[allow(dead_code)] // Future exact parser output constructor.
329    fn from_exact_parser(
330        subject: RouteArtifactBinding,
331        uuid: String,
332        interface_uuid: String,
333        deployment_platform: String,
334        fpga_part: String,
335        target: String,
336    ) -> PromotionResult<Self> {
337        let descriptor_identity_sha256 = xclbin_identity_sha256(
338            &subject,
339            &uuid,
340            &interface_uuid,
341            &deployment_platform,
342            &fpga_part,
343            &target,
344        )?;
345        Ok(Self {
346            subject,
347            uuid,
348            interface_uuid,
349            deployment_platform,
350            fpga_part,
351            target,
352            descriptor_identity_sha256,
353        })
354    }
355
356    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
357    pub(crate) const fn subject(&self) -> &RouteArtifactBinding {
358        &self.subject
359    }
360
361    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
362    pub(crate) fn uuid(&self) -> &str {
363        &self.uuid
364    }
365
366    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
367    pub(crate) fn interface_uuid(&self) -> &str {
368        &self.interface_uuid
369    }
370
371    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
372    pub(crate) fn deployment_platform(&self) -> &str {
373        &self.deployment_platform
374    }
375
376    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
377    pub(crate) fn fpga_part(&self) -> &str {
378        &self.fpga_part
379    }
380
381    #[allow(dead_code)] // Consumed by the later final-evidence adapter.
382    pub(crate) fn target(&self) -> &str {
383        &self.target
384    }
385}
386
387#[allow(dead_code)] // Kernel-only is an explicit adversarial parser output.
388#[derive(Clone, Copy, Debug, Eq, PartialEq)]
389enum ResourceScope {
390    WholeRoutedDesignIncludingShell,
391    KernelHierarchyOnly,
392}
393
394#[derive(Debug)]
395struct RouteIdentityObservation {
396    evidence_tier: String,
397    capture_kind: String,
398    status: String,
399    profile: String,
400    amd_release: String,
401    deployment_platform: String,
402    device_part: String,
403    route_design_digest: String,
404    requested_clock_hz: u64,
405    actual_routed_hz: u64,
406}
407
408/// Identity independently derived while revalidating the stage manifest.
409#[derive(Debug)]
410struct StageManifestIdentityObservation {
411    source: RouteArtifactBinding,
412    route_provenance: RouteObservationProvenance,
413    stage_subject_sha256: String,
414    design_digest: String,
415}
416
417/// Stage identity independently derived while revalidating the run inventory.
418#[derive(Debug)]
419struct RunInventoryIdentityObservation {
420    source: RouteArtifactBinding,
421    route_provenance: RouteObservationProvenance,
422    stage_subject_sha256: String,
423    design_digest: String,
424}
425
426/// Exact evidence subjects attached to one parser-derived route observation.
427///
428/// The future parser constructs this alongside its typed observation. Every
429/// subject is descriptor-derived and the raw-report closure is recomputed by
430/// this policy layer before the observation can be used.
431#[derive(Debug)]
432struct RouteObservationProvenance {
433    run_inventory: RouteArtifactBinding,
434    route_seal: RouteArtifactBinding,
435    raw_report_set_sha256: String,
436    source_report: RouteArtifactBinding,
437}
438
439/// One parser-derived observation inseparable from its exact evidence closure.
440#[derive(Debug)]
441struct BoundRouteObservation<T> {
442    provenance: RouteObservationProvenance,
443    value: T,
444}
445
446#[allow(clippy::struct_excessive_bools)] // Independent route-state observations.
447#[derive(Debug)]
448struct RouteStatusObservation {
449    placed_fully: bool,
450    routed_fully: bool,
451    partially_routed: bool,
452    route_errors: bool,
453}
454
455#[derive(Debug)]
456struct RouteTimingObservation {
457    setup_wns_femtoseconds: i64,
458    hold_whs_femtoseconds: i64,
459    setup_path_count: u64,
460    hold_path_count: u64,
461    unconstrained_path_count: u64,
462    unconstrained_endpoint_count: u64,
463    complete_timing_coverage: bool,
464}
465
466#[derive(Debug)]
467struct RouteDrcObservation {
468    complete_no_waivers_evaluation: bool,
469    enabled_error_violations: u64,
470    enabled_fatal_violations: u64,
471    installed_waivers: u64,
472}
473
474#[derive(Debug)]
475struct RouteResourceObservation {
476    scope: ResourceScope,
477    luts: u64,
478    registers: u64,
479    dsps: u64,
480    declared_lut_cap: u64,
481    declared_register_cap: u64,
482    declared_dsp_cap: u64,
483}
484
485#[allow(clippy::struct_excessive_bools)] // Lower-tier nonclaim fields are independent.
486#[derive(Debug)]
487struct RouteNonclaims {
488    hardware_execution: bool,
489    card_validated: bool,
490    cryptographic_outputs_valid: bool,
491    performance_validated: bool,
492    hardware_completion_promotable: bool,
493}
494
495/// Typed values that a future exact AMD-format parser must derive from retained
496/// route descriptors.
497///
498/// This is deliberately not a serde wire and defines no AMD syntax. The
499/// validator below checks only identity joins and route policy; descriptor and
500/// raw-format verification remain mandatory before constructing this input.
501#[derive(Debug)]
502struct RoutePolicyInputs {
503    identity: BoundRouteObservation<RouteIdentityObservation>,
504    stage_identity: StageManifestIdentityObservation,
505    inventory_identity: RunInventoryIdentityObservation,
506    status: BoundRouteObservation<RouteStatusObservation>,
507    timing: BoundRouteObservation<RouteTimingObservation>,
508    drc: BoundRouteObservation<RouteDrcObservation>,
509    resources: BoundRouteObservation<RouteResourceObservation>,
510    nonclaims: RouteNonclaims,
511    stage_manifest: RouteArtifactBinding,
512    run_inventory: RouteArtifactBinding,
513    route_seal: RouteArtifactBinding,
514    inventory_raw_reports: Vec<RouteArtifactBinding>,
515    seal_raw_reports: Vec<RouteArtifactBinding>,
516    routed_clock_evidence: RouteArtifactBinding,
517    xclbin_identity: RouteXclbinIdentity,
518    inventory_routed_clock_evidence: RouteArtifactBinding,
519    inventory_xclbin: RouteArtifactBinding,
520    seal_run_inventory: RouteArtifactBinding,
521    seal_routed_clock_evidence: RouteArtifactBinding,
522    seal_xclbin: RouteArtifactBinding,
523}
524
525/// Applies the accepted-route identity, timing, DRC, and whole-design resource
526/// policy to typed output from a future exact parser.
527///
528/// This function does not parse AMD bytes and is not called by the current
529/// public entry point. Keeping it module-private prevents a caller-authored DTO
530/// or an unrelated crate module from becoming route evidence.
531#[allow(dead_code, clippy::large_types_passed_by_value, clippy::too_many_lines)] // Connected only after the exact accepted-route parser lands.
532fn validate_route_policy_inputs(
533    request: &PromotionRequest,
534    input: RoutePolicyInputs,
535) -> PromotionResult<ValidatedRoutePolicy> {
536    validate_nonclaims(&input.nonclaims)?;
537
538    for (artifact, label) in [
539        (&input.stage_manifest, "route stage manifest"),
540        (&input.run_inventory, "route run inventory"),
541        (&input.route_seal, "accepted route seal"),
542        (&input.routed_clock_evidence, "routed-clock evidence"),
543        (&input.xclbin_identity.subject, "routed xclbin"),
544        (
545            &input.inventory_routed_clock_evidence,
546            "inventory routed-clock binding",
547        ),
548        (&input.inventory_xclbin, "inventory xclbin binding"),
549        (&input.seal_run_inventory, "route-seal inventory binding"),
550        (
551            &input.seal_routed_clock_evidence,
552            "route-seal routed-clock binding",
553        ),
554        (&input.seal_xclbin, "route-seal xclbin binding"),
555    ] {
556        artifact.validate(label)?;
557    }
558
559    let locators = request.route_locators();
560    input
561        .stage_manifest
562        .require_request_binding(locators.staged_run_manifest(), "route stage manifest")?;
563    input
564        .run_inventory
565        .require_request_binding(locators.complete_run_inventory(), "route run inventory")?;
566    input
567        .route_seal
568        .require_request_binding(locators.accepted_250mhz_route_seal(), "accepted route seal")?;
569    require_strict_child(
570        locators.staged_run_root().absolute_path(),
571        input.stage_manifest.absolute_path(),
572        "route stage manifest",
573    )?;
574    require_strict_child(
575        locators.acceptance_run_root().absolute_path(),
576        input.routed_clock_evidence.absolute_path(),
577        "routed-clock evidence",
578    )?;
579    require_strict_child(
580        locators.acceptance_run_root().absolute_path(),
581        input.xclbin_identity.subject.absolute_path(),
582        "routed xclbin",
583    )?;
584    require_same_artifact(
585        &input.run_inventory,
586        &input.seal_run_inventory,
587        "route seal to run inventory",
588    )?;
589    require_same_artifact(
590        &input.routed_clock_evidence,
591        &input.inventory_routed_clock_evidence,
592        "run inventory to routed-clock evidence",
593    )?;
594    require_same_artifact(
595        &input.routed_clock_evidence,
596        &input.seal_routed_clock_evidence,
597        "route seal to routed-clock evidence",
598    )?;
599    require_same_artifact(
600        &input.xclbin_identity.subject,
601        &input.inventory_xclbin,
602        "run inventory to xclbin",
603    )?;
604    require_same_artifact(
605        &input.xclbin_identity.subject,
606        &input.seal_xclbin,
607        "route seal to xclbin",
608    )?;
609    validate_raw_report_bindings(
610        locators.acceptance_run_root().absolute_path(),
611        &input.inventory_raw_reports,
612        &input.seal_raw_reports,
613    )?;
614
615    let mut paths = BTreeSet::new();
616    let mut physical_subjects = BTreeSet::new();
617    for artifact in [
618        &input.stage_manifest,
619        &input.run_inventory,
620        &input.route_seal,
621        &input.routed_clock_evidence,
622        &input.xclbin_identity.subject,
623    ] {
624        require_unique_artifact_subject(&mut paths, &mut physical_subjects, artifact)?;
625    }
626    for report in &input.inventory_raw_reports {
627        require_unique_artifact_subject(&mut paths, &mut physical_subjects, report)?;
628    }
629
630    let report_set_sha256 = canonical_raw_report_set_sha256(&input.inventory_raw_reports)?;
631    for (provenance, label) in [
632        (&input.identity.provenance, "route identity observation"),
633        (
634            &input.stage_identity.route_provenance,
635            "stage identity observation",
636        ),
637        (
638            &input.inventory_identity.route_provenance,
639            "run-inventory identity observation",
640        ),
641        (&input.status.provenance, "route status observation"),
642        (&input.timing.provenance, "route timing observation"),
643        (&input.drc.provenance, "route DRC observation"),
644        (&input.resources.provenance, "route resource observation"),
645    ] {
646        validate_observation_provenance(
647            provenance,
648            &input.run_inventory,
649            &input.route_seal,
650            &input.inventory_raw_reports,
651            &report_set_sha256,
652            label,
653        )?;
654    }
655
656    validate_identity(&input.identity.value)?;
657    validate_route_status(&input.status.value)?;
658    validate_timing(&input.timing.value)?;
659    validate_drc(&input.drc.value)?;
660    validate_resources(&input.resources.value)?;
661    validate_stage_identity_join(
662        &input.identity.value,
663        &input.stage_identity,
664        &input.inventory_identity,
665        &input.stage_manifest,
666        &input.run_inventory,
667    )?;
668    validate_xclbin_identity(&input.identity.value, &input.xclbin_identity)?;
669
670    Ok(ValidatedRoutePolicy {
671        design_digest: input.identity.value.route_design_digest,
672        actual_routed_hz: input.identity.value.actual_routed_hz,
673        setup_wns_femtoseconds: input.timing.value.setup_wns_femtoseconds,
674        hold_whs_femtoseconds: input.timing.value.hold_whs_femtoseconds,
675        whole_design_luts: input.resources.value.luts,
676        whole_design_registers: input.resources.value.registers,
677        whole_design_dsps: input.resources.value.dsps,
678        stage_manifest: input.stage_manifest,
679        run_inventory: input.run_inventory,
680        route_seal: input.route_seal,
681        raw_reports: input.inventory_raw_reports,
682        routed_clock_evidence: input.routed_clock_evidence,
683        xclbin_identity: input.xclbin_identity,
684    })
685}
686
687fn validate_identity(identity: &RouteIdentityObservation) -> PromotionResult<()> {
688    let fixed_identity = identity.evidence_tier == ACCEPTANCE_EVIDENCE_TIER
689        && identity.capture_kind == ACCEPTANCE_CAPTURE_KIND
690        && identity.status == ACCEPTANCE_STATUS
691        && identity.profile == PERFORMANCE_PROFILE
692        && identity.amd_release == REQUIRED_AMD_RELEASE
693        && identity.deployment_platform == REQUIRED_PLATFORM
694        && identity.device_part == REQUIRED_DEVICE_PART
695        && is_lower_hex(&identity.route_design_digest, 64, true)
696        && identity.requested_clock_hz == BASELINE_CLOCK_HZ;
697    if !fixed_identity {
698        return Err(PromotionError::new(
699            FailureCode::RouteUnaccepted,
700            "route parser output does not identify the exact SHA-256 U280 250 MHz acceptance design",
701        ));
702    }
703    if identity.actual_routed_hz < BASELINE_CLOCK_HZ {
704        return Err(PromotionError::new(
705            FailureCode::RouteTiming,
706            format!(
707                "actual routed clock {} Hz is below the required {} Hz baseline",
708                identity.actual_routed_hz, BASELINE_CLOCK_HZ
709            ),
710        ));
711    }
712    Ok(())
713}
714
715fn validate_stage_identity_join(
716    route: &RouteIdentityObservation,
717    stage: &StageManifestIdentityObservation,
718    inventory: &RunInventoryIdentityObservation,
719    stage_manifest: &RouteArtifactBinding,
720    run_inventory: &RouteArtifactBinding,
721) -> PromotionResult<()> {
722    stage.source.validate("stage identity source")?;
723    inventory.source.validate("run-inventory identity source")?;
724    let canonical = is_lower_hex(&stage.stage_subject_sha256, 64, true)
725        && is_lower_hex(&inventory.stage_subject_sha256, 64, true)
726        && is_lower_hex(&stage.design_digest, 64, true)
727        && is_lower_hex(&inventory.design_digest, 64, true);
728    if !canonical {
729        return Err(PromotionError::new(
730            FailureCode::Noncanonical,
731            "stage-manifest or run-inventory identity contains a non-canonical digest",
732        ));
733    }
734    if &stage.source != stage_manifest
735        || &inventory.source != run_inventory
736        || stage.stage_subject_sha256 != stage_manifest.sha256
737        || inventory.stage_subject_sha256 != stage.stage_subject_sha256
738        || stage.design_digest != route.route_design_digest
739        || inventory.design_digest != stage.design_digest
740    {
741        return Err(PromotionError::new(
742            FailureCode::IdentityJoin,
743            "stage manifest and run inventory do not independently join one stage subject and design family",
744        ));
745    }
746    Ok(())
747}
748
749fn validate_xclbin_identity(
750    route: &RouteIdentityObservation,
751    xclbin: &RouteXclbinIdentity,
752) -> PromotionResult<()> {
753    if !is_canonical_uuid(&xclbin.uuid) || !is_canonical_uuid(&xclbin.interface_uuid) {
754        return Err(PromotionError::new(
755            FailureCode::Noncanonical,
756            "route-derived xclbin UUID or interface UUID is not canonical",
757        ));
758    }
759    if xclbin.deployment_platform != route.deployment_platform
760        || xclbin.fpga_part != route.device_part
761    {
762        return Err(PromotionError::new(
763            FailureCode::IdentityJoin,
764            "route-derived xclbin platform or FPGA part differs from the accepted route",
765        ));
766    }
767    if xclbin.target != REQUIRED_XCLBIN_TARGET {
768        return Err(PromotionError::new(
769            FailureCode::RouteUnaccepted,
770            "accepted route xclbin target is not hw",
771        ));
772    }
773    if !is_lower_hex(&xclbin.descriptor_identity_sha256, 64, true)
774        || xclbin.descriptor_identity_sha256
775            != xclbin_identity_sha256(
776                &xclbin.subject,
777                &xclbin.uuid,
778                &xclbin.interface_uuid,
779                &xclbin.deployment_platform,
780                &xclbin.fpga_part,
781                &xclbin.target,
782            )?
783    {
784        return Err(PromotionError::new(
785            FailureCode::IdentityJoin,
786            "route-derived xclbin identity is not bound to its exact descriptor subject",
787        ));
788    }
789    Ok(())
790}
791
792fn validate_route_status(status: &RouteStatusObservation) -> PromotionResult<()> {
793    if !status.placed_fully
794        || !status.routed_fully
795        || status.partially_routed
796        || status.route_errors
797    {
798        return Err(PromotionError::new(
799            FailureCode::RouteTiming,
800            "accepted route must be completely placed and routed with no route errors",
801        ));
802    }
803    Ok(())
804}
805
806fn validate_timing(timing: &RouteTimingObservation) -> PromotionResult<()> {
807    if !timing.complete_timing_coverage
808        || timing.setup_path_count == 0
809        || timing.hold_path_count == 0
810        || timing.unconstrained_path_count != 0
811        || timing.unconstrained_endpoint_count != 0
812        || timing.setup_wns_femtoseconds < 0
813        || timing.hold_whs_femtoseconds < 0
814    {
815        return Err(PromotionError::new(
816            FailureCode::RouteTiming,
817            "accepted route lacks complete constrained setup/hold coverage or has negative slack",
818        ));
819    }
820    Ok(())
821}
822
823fn validate_drc(drc: &RouteDrcObservation) -> PromotionResult<()> {
824    if !drc.complete_no_waivers_evaluation
825        || drc.enabled_error_violations != 0
826        || drc.enabled_fatal_violations != 0
827        || drc.installed_waivers != 0
828    {
829        return Err(PromotionError::new(
830            FailureCode::RouteTiming,
831            "accepted route lacks a complete no-waivers DRC pass",
832        ));
833    }
834    Ok(())
835}
836
837fn validate_resources(resources: &RouteResourceObservation) -> PromotionResult<()> {
838    let fixed_caps = resources.declared_lut_cap == MAXIMUM_LUTS
839        && resources.declared_register_cap == MAXIMUM_REGISTERS
840        && resources.declared_dsp_cap == MAXIMUM_DSPS;
841    let within_caps = resources.luts <= MAXIMUM_LUTS
842        && resources.registers <= MAXIMUM_REGISTERS
843        && resources.dsps <= MAXIMUM_DSPS;
844    if resources.scope != ResourceScope::WholeRoutedDesignIncludingShell
845        || !fixed_caps
846        || !within_caps
847    {
848        return Err(PromotionError::new(
849            FailureCode::RouteResource,
850            "route resources must cover the whole routed design including shell and satisfy the fixed 200k LUT/300k register/4k DSP policy",
851        ));
852    }
853    Ok(())
854}
855
856fn validate_nonclaims(nonclaims: &RouteNonclaims) -> PromotionResult<()> {
857    if nonclaims.hardware_execution
858        || nonclaims.card_validated
859        || nonclaims.cryptographic_outputs_valid
860        || nonclaims.performance_validated
861        || nonclaims.hardware_completion_promotable
862    {
863        return Err(PromotionError::new(
864            FailureCode::PrematureClaim,
865            "route-tier input contains a premature hardware, card, cryptographic, performance, or completion claim",
866        ));
867    }
868    Ok(())
869}
870
871fn require_strict_child(root: &str, path: &str, label: &str) -> PromotionResult<()> {
872    let prefix = format!("{root}/");
873    if !path.starts_with(&prefix) {
874        return Err(PromotionError::new(
875            FailureCode::IdentityJoin,
876            format!("{label} is not beneath the exact acceptance-run root"),
877        ));
878    }
879    Ok(())
880}
881
882fn require_same_artifact(
883    expected: &RouteArtifactBinding,
884    observed: &RouteArtifactBinding,
885    label: &str,
886) -> PromotionResult<()> {
887    if expected != observed {
888        return Err(PromotionError::new(
889            FailureCode::IdentityJoin,
890            format!("{label} does not repeat one exact artifact identity"),
891        ));
892    }
893    Ok(())
894}
895
896fn validate_raw_report_bindings(
897    acceptance_root: &str,
898    inventory_reports: &[RouteArtifactBinding],
899    seal_reports: &[RouteArtifactBinding],
900) -> PromotionResult<()> {
901    validate_raw_report_set(
902        acceptance_root,
903        inventory_reports,
904        "run-inventory raw reports",
905    )?;
906    validate_raw_report_set(acceptance_root, seal_reports, "route-seal raw reports")?;
907    if inventory_reports != seal_reports {
908        return Err(PromotionError::new(
909            FailureCode::IdentityJoin,
910            "route seal does not bind the exact run-inventory raw-report set",
911        ));
912    }
913    Ok(())
914}
915
916fn validate_raw_report_set(
917    acceptance_root: &str,
918    reports: &[RouteArtifactBinding],
919    label: &str,
920) -> PromotionResult<()> {
921    if reports.is_empty() || reports.len() > MAXIMUM_RAW_ROUTE_REPORTS {
922        return Err(PromotionError::new(
923            FailureCode::RouteUnaccepted,
924            format!("{label} must contain 1..={MAXIMUM_RAW_ROUTE_REPORTS} exact artifacts"),
925        ));
926    }
927    let mut previous_path: Option<&str> = None;
928    for (index, report) in reports.iter().enumerate() {
929        report.validate(&format!("{label}[{index}]"))?;
930        require_strict_child(
931            acceptance_root,
932            report.absolute_path(),
933            &format!("{label}[{index}]"),
934        )?;
935        if let Some(previous) = previous_path {
936            if previous == report.absolute_path() {
937                return Err(PromotionError::new(
938                    FailureCode::IdentityJoin,
939                    format!("{label} aliases a raw-report path"),
940                ));
941            }
942            if previous > report.absolute_path() {
943                return Err(PromotionError::new(
944                    FailureCode::Noncanonical,
945                    format!("{label} is not strictly sorted by canonical absolute path"),
946                ));
947            }
948        }
949        previous_path = Some(report.absolute_path());
950    }
951    Ok(())
952}
953
954fn validate_observation_provenance(
955    provenance: &RouteObservationProvenance,
956    run_inventory: &RouteArtifactBinding,
957    route_seal: &RouteArtifactBinding,
958    raw_reports: &[RouteArtifactBinding],
959    raw_report_set_sha256: &str,
960    label: &str,
961) -> PromotionResult<()> {
962    provenance
963        .run_inventory
964        .validate(&format!("{label} run-inventory subject"))?;
965    provenance
966        .route_seal
967        .validate(&format!("{label} route-seal subject"))?;
968    provenance
969        .source_report
970        .validate(&format!("{label} source-report subject"))?;
971    if !is_lower_hex(&provenance.raw_report_set_sha256, 64, true) {
972        return Err(PromotionError::new(
973            FailureCode::Noncanonical,
974            format!("{label} has a non-canonical raw-report closure digest"),
975        ));
976    }
977    if &provenance.run_inventory != run_inventory
978        || &provenance.route_seal != route_seal
979        || provenance.raw_report_set_sha256 != raw_report_set_sha256
980        || !raw_reports
981            .iter()
982            .any(|report| report == &provenance.source_report)
983    {
984        return Err(PromotionError::new(
985            FailureCode::IdentityJoin,
986            format!(
987                "{label} is not bound to the exact inventory, seal, raw-report closure, and source report"
988            ),
989        ));
990    }
991    Ok(())
992}
993
994fn canonical_raw_report_set_sha256(reports: &[RouteArtifactBinding]) -> PromotionResult<String> {
995    let mut hasher = Sha256::new();
996    hasher.update(REPORT_SET_DIGEST_DOMAIN);
997    let report_count = u64::try_from(reports.len()).map_err(|_| {
998        PromotionError::new(
999            FailureCode::RouteUnaccepted,
1000            "raw-report set exceeds the canonical digest count boundary",
1001        )
1002    })?;
1003    hasher.update(report_count.to_be_bytes());
1004    for report in reports {
1005        update_artifact_subject_digest(&mut hasher, report)?;
1006    }
1007    Ok(hex_lower(&hasher.finalize()))
1008}
1009
1010fn xclbin_identity_sha256(
1011    subject: &RouteArtifactBinding,
1012    uuid: &str,
1013    interface_uuid: &str,
1014    deployment_platform: &str,
1015    fpga_part: &str,
1016    target: &str,
1017) -> PromotionResult<String> {
1018    let mut hasher = Sha256::new();
1019    hasher.update(XCLBIN_IDENTITY_DIGEST_DOMAIN);
1020    update_artifact_subject_digest(&mut hasher, subject)?;
1021    for value in [uuid, interface_uuid, deployment_platform, fpga_part, target] {
1022        update_length_prefixed(&mut hasher, value.as_bytes())?;
1023    }
1024    Ok(hex_lower(&hasher.finalize()))
1025}
1026
1027fn update_artifact_subject_digest(
1028    hasher: &mut Sha256,
1029    artifact: &RouteArtifactBinding,
1030) -> PromotionResult<()> {
1031    update_length_prefixed(hasher, artifact.absolute_path.as_bytes())?;
1032    update_length_prefixed(hasher, artifact.descriptor.canonical_realpath.as_bytes())?;
1033    hasher.update(artifact.bytes.to_be_bytes());
1034    update_length_prefixed(hasher, artifact.sha256.as_bytes())?;
1035    hasher.update(artifact.descriptor.device.to_be_bytes());
1036    hasher.update(artifact.descriptor.inode.to_be_bytes());
1037    Ok(())
1038}
1039
1040fn update_length_prefixed(hasher: &mut Sha256, value: &[u8]) -> PromotionResult<()> {
1041    let length = u64::try_from(value.len()).map_err(|_| {
1042        PromotionError::new(
1043            FailureCode::RouteUnaccepted,
1044            "route evidence subject exceeds the canonical digest length boundary",
1045        )
1046    })?;
1047    hasher.update(length.to_be_bytes());
1048    hasher.update(value);
1049    Ok(())
1050}
1051
1052fn require_unique_artifact_subject<'a>(
1053    paths: &mut BTreeSet<&'a str>,
1054    physical_subjects: &mut BTreeSet<(u64, u64)>,
1055    artifact: &'a RouteArtifactBinding,
1056) -> PromotionResult<()> {
1057    if !paths.insert(artifact.absolute_path()) {
1058        return Err(PromotionError::new(
1059            FailureCode::IdentityJoin,
1060            "accepted route proof lexically aliases independently bound artifacts",
1061        ));
1062    }
1063    if !physical_subjects.insert((artifact.descriptor.device, artifact.descriptor.inode)) {
1064        return Err(PromotionError::new(
1065            FailureCode::IdentityJoin,
1066            "accepted route proof aliases descriptor subjects by device and inode",
1067        ));
1068    }
1069    Ok(())
1070}
1071
1072fn is_canonical_uuid(value: &str) -> bool {
1073    let bytes = value.as_bytes();
1074    bytes.len() == 36
1075        && bytes.iter().enumerate().all(|(index, byte)| {
1076            if matches!(index, 8 | 13 | 18 | 23) {
1077                *byte == b'-'
1078            } else {
1079                byte.is_ascii_digit() || (b'a'..=b'f').contains(byte)
1080            }
1081        })
1082        && bytes
1083            .iter()
1084            .enumerate()
1085            .any(|(index, byte)| !matches!(index, 8 | 13 | 18 | 23) && *byte != b'0')
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091    use crate::model::synthetic_request;
1092
1093    const XCLBIN_UUID: &str = "11111111-2222-3333-4444-555555555555";
1094    const INTERFACE_UUID: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
1095
1096    fn subject(
1097        absolute_path: &str,
1098        bytes: u64,
1099        sha256: String,
1100        inode: u64,
1101    ) -> RouteArtifactBinding {
1102        RouteArtifactBinding::from_exact_parser(
1103            absolute_path.to_owned(),
1104            absolute_path.to_owned(),
1105            bytes,
1106            sha256,
1107            7,
1108            inode,
1109        )
1110    }
1111
1112    fn artifact(locator: &RequestFileLocator<'_>, inode: u64) -> RouteArtifactBinding {
1113        subject(
1114            locator.absolute_path(),
1115            locator.bytes(),
1116            locator.sha256().to_owned(),
1117            inode,
1118        )
1119    }
1120
1121    fn repeat_artifact(artifact: &RouteArtifactBinding) -> RouteArtifactBinding {
1122        RouteArtifactBinding::from_exact_parser(
1123            artifact.absolute_path.clone(),
1124            artifact.descriptor.canonical_realpath.clone(),
1125            artifact.bytes,
1126            artifact.sha256.clone(),
1127            artifact.descriptor.device,
1128            artifact.descriptor.inode,
1129        )
1130    }
1131
1132    fn clock_artifact() -> RouteArtifactBinding {
1133        subject(
1134            "/evidence/route-acceptance/evidence/post-route-evidence.json",
1135            100,
1136            "b".repeat(64),
1137            30,
1138        )
1139    }
1140
1141    fn xclbin_artifact() -> RouteArtifactBinding {
1142        subject(
1143            "/evidence/route-acceptance/output/kernel.xclbin",
1144            1_000,
1145            "c".repeat(64),
1146            40,
1147        )
1148    }
1149
1150    fn raw_report_artifacts() -> Vec<RouteArtifactBinding> {
1151        vec![
1152            subject(
1153                "/evidence/route-acceptance/reports/report-a.raw",
1154                200,
1155                "d".repeat(64),
1156                20,
1157            ),
1158            subject(
1159                "/evidence/route-acceptance/reports/report-b.raw",
1160                300,
1161                "e".repeat(64),
1162                21,
1163            ),
1164        ]
1165    }
1166
1167    fn xclbin_identity() -> RouteXclbinIdentity {
1168        RouteXclbinIdentity::from_exact_parser(
1169            xclbin_artifact(),
1170            XCLBIN_UUID.to_owned(),
1171            INTERFACE_UUID.to_owned(),
1172            REQUIRED_PLATFORM.to_owned(),
1173            REQUIRED_DEVICE_PART.to_owned(),
1174            REQUIRED_XCLBIN_TARGET.to_owned(),
1175        )
1176        .unwrap()
1177    }
1178
1179    fn provenance(
1180        run_inventory: &RouteArtifactBinding,
1181        route_seal: &RouteArtifactBinding,
1182        raw_reports: &[RouteArtifactBinding],
1183        source_index: usize,
1184    ) -> RouteObservationProvenance {
1185        RouteObservationProvenance {
1186            run_inventory: repeat_artifact(run_inventory),
1187            route_seal: repeat_artifact(route_seal),
1188            raw_report_set_sha256: canonical_raw_report_set_sha256(raw_reports).unwrap(),
1189            source_report: repeat_artifact(&raw_reports[source_index]),
1190        }
1191    }
1192
1193    #[allow(clippy::too_many_lines)] // Explicitly assembles every independent parser output.
1194    fn fixture(request: &PromotionRequest) -> RoutePolicyInputs {
1195        let locators = request.route_locators();
1196        let stage_manifest = artifact(locators.staged_run_manifest(), 1);
1197        let run_inventory = artifact(locators.complete_run_inventory(), 2);
1198        let route_seal = artifact(locators.accepted_250mhz_route_seal(), 3);
1199        let inventory_raw_reports = raw_report_artifacts();
1200        let seal_raw_reports = raw_report_artifacts();
1201        let routed_clock_evidence = clock_artifact();
1202        let xclbin_identity = xclbin_identity();
1203        let inventory_routed_clock_evidence = repeat_artifact(&routed_clock_evidence);
1204        let seal_routed_clock_evidence = repeat_artifact(&routed_clock_evidence);
1205        let inventory_xclbin = repeat_artifact(xclbin_identity.subject());
1206        let seal_xclbin = repeat_artifact(xclbin_identity.subject());
1207        let seal_run_inventory = repeat_artifact(&run_inventory);
1208        RoutePolicyInputs {
1209            identity: BoundRouteObservation {
1210                provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1211                value: RouteIdentityObservation {
1212                    evidence_tier: ACCEPTANCE_EVIDENCE_TIER.to_owned(),
1213                    capture_kind: ACCEPTANCE_CAPTURE_KIND.to_owned(),
1214                    status: ACCEPTANCE_STATUS.to_owned(),
1215                    profile: PERFORMANCE_PROFILE.to_owned(),
1216                    amd_release: REQUIRED_AMD_RELEASE.to_owned(),
1217                    deployment_platform: REQUIRED_PLATFORM.to_owned(),
1218                    device_part: REQUIRED_DEVICE_PART.to_owned(),
1219                    route_design_digest: "a".repeat(64),
1220                    requested_clock_hz: BASELINE_CLOCK_HZ,
1221                    actual_routed_hz: BASELINE_CLOCK_HZ,
1222                },
1223            },
1224            stage_identity: StageManifestIdentityObservation {
1225                source: repeat_artifact(&stage_manifest),
1226                route_provenance: provenance(
1227                    &run_inventory,
1228                    &route_seal,
1229                    &inventory_raw_reports,
1230                    0,
1231                ),
1232                stage_subject_sha256: locators.staged_run_manifest().sha256().to_owned(),
1233                design_digest: "a".repeat(64),
1234            },
1235            inventory_identity: RunInventoryIdentityObservation {
1236                source: repeat_artifact(&run_inventory),
1237                route_provenance: provenance(
1238                    &run_inventory,
1239                    &route_seal,
1240                    &inventory_raw_reports,
1241                    0,
1242                ),
1243                stage_subject_sha256: locators.staged_run_manifest().sha256().to_owned(),
1244                design_digest: "a".repeat(64),
1245            },
1246            status: BoundRouteObservation {
1247                provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1248                value: RouteStatusObservation {
1249                    placed_fully: true,
1250                    routed_fully: true,
1251                    partially_routed: false,
1252                    route_errors: false,
1253                },
1254            },
1255            timing: BoundRouteObservation {
1256                provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 0),
1257                value: RouteTimingObservation {
1258                    setup_wns_femtoseconds: 0,
1259                    hold_whs_femtoseconds: 0,
1260                    setup_path_count: 1,
1261                    hold_path_count: 1,
1262                    unconstrained_path_count: 0,
1263                    unconstrained_endpoint_count: 0,
1264                    complete_timing_coverage: true,
1265                },
1266            },
1267            drc: BoundRouteObservation {
1268                provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 1),
1269                value: RouteDrcObservation {
1270                    complete_no_waivers_evaluation: true,
1271                    enabled_error_violations: 0,
1272                    enabled_fatal_violations: 0,
1273                    installed_waivers: 0,
1274                },
1275            },
1276            resources: BoundRouteObservation {
1277                provenance: provenance(&run_inventory, &route_seal, &inventory_raw_reports, 1),
1278                value: RouteResourceObservation {
1279                    scope: ResourceScope::WholeRoutedDesignIncludingShell,
1280                    luts: MAXIMUM_LUTS,
1281                    registers: MAXIMUM_REGISTERS,
1282                    dsps: MAXIMUM_DSPS,
1283                    declared_lut_cap: MAXIMUM_LUTS,
1284                    declared_register_cap: MAXIMUM_REGISTERS,
1285                    declared_dsp_cap: MAXIMUM_DSPS,
1286                },
1287            },
1288            nonclaims: RouteNonclaims {
1289                hardware_execution: false,
1290                card_validated: false,
1291                cryptographic_outputs_valid: false,
1292                performance_validated: false,
1293                hardware_completion_promotable: false,
1294            },
1295            stage_manifest,
1296            run_inventory,
1297            route_seal,
1298            inventory_raw_reports,
1299            seal_raw_reports,
1300            routed_clock_evidence,
1301            xclbin_identity,
1302            inventory_routed_clock_evidence,
1303            inventory_xclbin,
1304            seal_run_inventory,
1305            seal_routed_clock_evidence,
1306            seal_xclbin,
1307        }
1308    }
1309
1310    #[allow(clippy::large_types_passed_by_value)] // The validator consumes parser output.
1311    fn assert_rejected(input: RoutePolicyInputs, code: FailureCode) {
1312        let request = synthetic_request(&"1".repeat(32));
1313        assert_eq!(
1314            validate_route_policy_inputs(&request, input)
1315                .expect_err("mutated route proof must fail closed")
1316                .code(),
1317            code
1318        );
1319    }
1320
1321    #[test]
1322    fn exact_policy_boundary_produces_metadata_not_accepted_evidence() {
1323        let request = synthetic_request(&"1".repeat(32));
1324        let policy = validate_route_policy_inputs(&request, fixture(&request)).unwrap();
1325        assert_eq!(policy.design_digest, "a".repeat(64));
1326        assert_eq!(policy.actual_routed_hz, BASELINE_CLOCK_HZ);
1327        assert_eq!(policy.setup_wns_femtoseconds, 0);
1328        assert_eq!(policy.hold_whs_femtoseconds, 0);
1329        assert_eq!(policy.whole_design_luts, MAXIMUM_LUTS);
1330        assert_eq!(policy.whole_design_registers, MAXIMUM_REGISTERS);
1331        assert_eq!(policy.whole_design_dsps, MAXIMUM_DSPS);
1332        assert_eq!(policy.raw_reports.len(), 2);
1333        assert_eq!(policy.raw_reports[0].sha256(), "d".repeat(64));
1334        assert_eq!(policy.xclbin_identity.subject().bytes(), 1_000);
1335        assert_eq!(policy.xclbin_identity.subject().sha256(), "c".repeat(64));
1336        assert_eq!(
1337            policy.xclbin_identity.subject().canonical_realpath(),
1338            "/evidence/route-acceptance/output/kernel.xclbin"
1339        );
1340        assert_eq!(policy.xclbin_identity.subject().device(), 7);
1341        assert_eq!(policy.xclbin_identity.subject().inode(), 40);
1342        assert_eq!(policy.xclbin_identity.uuid(), XCLBIN_UUID);
1343        assert_eq!(policy.xclbin_identity.interface_uuid(), INTERFACE_UUID);
1344        assert_eq!(
1345            policy.xclbin_identity.deployment_platform(),
1346            REQUIRED_PLATFORM
1347        );
1348        assert_eq!(policy.xclbin_identity.fpga_part(), REQUIRED_DEVICE_PART);
1349        assert_eq!(policy.xclbin_identity.target(), REQUIRED_XCLBIN_TARGET);
1350    }
1351
1352    #[test]
1353    fn routed_clock_above_the_baseline_remains_acceptable() {
1354        let request = synthetic_request(&"1".repeat(32));
1355        let mut input = fixture(&request);
1356        input.identity.value.actual_routed_hz = BASELINE_CLOCK_HZ + 1;
1357        let policy = validate_route_policy_inputs(&request, input).unwrap();
1358        assert_eq!(policy.actual_routed_hz, BASELINE_CLOCK_HZ + 1);
1359    }
1360
1361    #[test]
1362    fn public_route_entry_remains_unreachable() {
1363        let request = synthetic_request(&"1".repeat(32));
1364        assert_eq!(
1365            require_accepted_route(&request).unwrap_err().code(),
1366            FailureCode::RouteUnaccepted
1367        );
1368    }
1369
1370    #[test]
1371    fn identity_mutations_fail_before_route_acceptance() {
1372        let request = synthetic_request(&"1".repeat(32));
1373
1374        let mut wrong_tier = fixture(&request);
1375        wrong_tier.identity.value.evidence_tier = "format-probe-capture".to_owned();
1376        assert_rejected(wrong_tier, FailureCode::RouteUnaccepted);
1377
1378        let mut wrong_capture = fixture(&request);
1379        wrong_capture.identity.value.capture_kind = "format-probe-capture".to_owned();
1380        assert_rejected(wrong_capture, FailureCode::RouteUnaccepted);
1381
1382        let mut failed_status = fixture(&request);
1383        failed_status.identity.value.status = "failed".to_owned();
1384        assert_rejected(failed_status, FailureCode::RouteUnaccepted);
1385
1386        let mut wrong_profile = fixture(&request);
1387        wrong_profile.identity.value.profile = "LEGACY_KECCAK".to_owned();
1388        assert_rejected(wrong_profile, FailureCode::RouteUnaccepted);
1389
1390        let mut wrong_release = fixture(&request);
1391        wrong_release.identity.value.amd_release = "2024.1".to_owned();
1392        assert_rejected(wrong_release, FailureCode::RouteUnaccepted);
1393
1394        let mut wrong_platform = fixture(&request);
1395        wrong_platform.identity.value.deployment_platform = "other-platform".to_owned();
1396        assert_rejected(wrong_platform, FailureCode::RouteUnaccepted);
1397
1398        let mut wrong_part = fixture(&request);
1399        wrong_part.identity.value.device_part = "other-part".to_owned();
1400        assert_rejected(wrong_part, FailureCode::RouteUnaccepted);
1401
1402        let mut zero_design = fixture(&request);
1403        zero_design.identity.value.route_design_digest = "0".repeat(64);
1404        assert_rejected(zero_design, FailureCode::RouteUnaccepted);
1405
1406        let mut mismatched_design = fixture(&request);
1407        mismatched_design.identity.value.route_design_digest = "d".repeat(64);
1408        assert_rejected(mismatched_design, FailureCode::IdentityJoin);
1409
1410        let mut changed_stage_subject = fixture(&request);
1411        changed_stage_subject.stage_identity.stage_subject_sha256 = "d".repeat(64);
1412        assert_rejected(changed_stage_subject, FailureCode::IdentityJoin);
1413
1414        let mut changed_inventory_subject = fixture(&request);
1415        changed_inventory_subject
1416            .inventory_identity
1417            .stage_subject_sha256 = "d".repeat(64);
1418        assert_rejected(changed_inventory_subject, FailureCode::IdentityJoin);
1419
1420        let mut changed_stage_design = fixture(&request);
1421        changed_stage_design.stage_identity.design_digest = "d".repeat(64);
1422        assert_rejected(changed_stage_design, FailureCode::IdentityJoin);
1423
1424        let mut changed_inventory_design = fixture(&request);
1425        changed_inventory_design.inventory_identity.design_digest = "d".repeat(64);
1426        assert_rejected(changed_inventory_design, FailureCode::IdentityJoin);
1427
1428        let mut noncanonical_stage_subject = fixture(&request);
1429        noncanonical_stage_subject
1430            .stage_identity
1431            .stage_subject_sha256 = "0".repeat(64);
1432        assert_rejected(noncanonical_stage_subject, FailureCode::Noncanonical);
1433
1434        let mut probe_clock = fixture(&request);
1435        probe_clock.identity.value.requested_clock_hz = 300_000_000;
1436        assert_rejected(probe_clock, FailureCode::RouteUnaccepted);
1437
1438        let mut slow_route = fixture(&request);
1439        slow_route.identity.value.actual_routed_hz = BASELINE_CLOCK_HZ - 1;
1440        assert_rejected(slow_route, FailureCode::RouteTiming);
1441    }
1442
1443    #[test]
1444    fn every_parsed_observation_rejects_subject_or_closure_splicing() {
1445        let request = synthetic_request(&"1".repeat(32));
1446
1447        let mut identity_inventory_splice = fixture(&request);
1448        identity_inventory_splice
1449            .identity
1450            .provenance
1451            .run_inventory
1452            .sha256 = "f".repeat(64);
1453        assert_rejected(identity_inventory_splice, FailureCode::IdentityJoin);
1454
1455        let mut status_source_splice = fixture(&request);
1456        status_source_splice.status.provenance.source_report = xclbin_artifact();
1457        assert_rejected(status_source_splice, FailureCode::IdentityJoin);
1458
1459        let mut timing_seal_splice = fixture(&request);
1460        timing_seal_splice.timing.provenance.route_seal.bytes += 1;
1461        assert_rejected(timing_seal_splice, FailureCode::IdentityJoin);
1462
1463        let mut timing_same_bytes_other_route = fixture(&request);
1464        timing_same_bytes_other_route
1465            .timing
1466            .provenance
1467            .source_report
1468            .descriptor
1469            .inode += 100;
1470        assert_rejected(timing_same_bytes_other_route, FailureCode::IdentityJoin);
1471
1472        let mut drc_closure_splice = fixture(&request);
1473        drc_closure_splice.drc.provenance.raw_report_set_sha256 = "f".repeat(64);
1474        assert_rejected(drc_closure_splice, FailureCode::IdentityJoin);
1475
1476        let mut resource_source_splice = fixture(&request);
1477        resource_source_splice.resources.provenance.source_report = subject(
1478            "/evidence/route-acceptance/reports/other-report.raw",
1479            400,
1480            "f".repeat(64),
1481            22,
1482        );
1483        assert_rejected(resource_source_splice, FailureCode::IdentityJoin);
1484
1485        let mut stage_source_splice = fixture(&request);
1486        let inventory_subject = repeat_artifact(&stage_source_splice.run_inventory);
1487        stage_source_splice.stage_identity.source = inventory_subject;
1488        assert_rejected(stage_source_splice, FailureCode::IdentityJoin);
1489
1490        let mut stage_closure_splice = fixture(&request);
1491        stage_closure_splice
1492            .stage_identity
1493            .route_provenance
1494            .raw_report_set_sha256 = "f".repeat(64);
1495        assert_rejected(stage_closure_splice, FailureCode::IdentityJoin);
1496
1497        let mut inventory_source_splice = fixture(&request);
1498        let stage_subject = repeat_artifact(&inventory_source_splice.stage_manifest);
1499        inventory_source_splice.inventory_identity.source = stage_subject;
1500        assert_rejected(inventory_source_splice, FailureCode::IdentityJoin);
1501    }
1502
1503    #[test]
1504    fn incomplete_route_timing_and_drc_fail_closed() {
1505        let request = synthetic_request(&"1".repeat(32));
1506
1507        let mut partial = fixture(&request);
1508        partial.status.value.partially_routed = true;
1509        assert_rejected(partial, FailureCode::RouteTiming);
1510
1511        let mut unplaced = fixture(&request);
1512        unplaced.status.value.placed_fully = false;
1513        assert_rejected(unplaced, FailureCode::RouteTiming);
1514
1515        let mut unrouted = fixture(&request);
1516        unrouted.status.value.routed_fully = false;
1517        assert_rejected(unrouted, FailureCode::RouteTiming);
1518
1519        let mut route_error = fixture(&request);
1520        route_error.status.value.route_errors = true;
1521        assert_rejected(route_error, FailureCode::RouteTiming);
1522
1523        let mut negative_setup = fixture(&request);
1524        negative_setup.timing.value.setup_wns_femtoseconds = -1;
1525        assert_rejected(negative_setup, FailureCode::RouteTiming);
1526
1527        let mut negative_hold = fixture(&request);
1528        negative_hold.timing.value.hold_whs_femtoseconds = -1;
1529        assert_rejected(negative_hold, FailureCode::RouteTiming);
1530
1531        let mut incomplete_coverage = fixture(&request);
1532        incomplete_coverage.timing.value.complete_timing_coverage = false;
1533        assert_rejected(incomplete_coverage, FailureCode::RouteTiming);
1534
1535        let mut missing_hold = fixture(&request);
1536        missing_hold.timing.value.hold_path_count = 0;
1537        assert_rejected(missing_hold, FailureCode::RouteTiming);
1538
1539        let mut missing_setup = fixture(&request);
1540        missing_setup.timing.value.setup_path_count = 0;
1541        assert_rejected(missing_setup, FailureCode::RouteTiming);
1542
1543        let mut unconstrained = fixture(&request);
1544        unconstrained.timing.value.unconstrained_endpoint_count = 1;
1545        assert_rejected(unconstrained, FailureCode::RouteTiming);
1546
1547        let mut unconstrained_path = fixture(&request);
1548        unconstrained_path.timing.value.unconstrained_path_count = 1;
1549        assert_rejected(unconstrained_path, FailureCode::RouteTiming);
1550
1551        let mut incomplete_drc = fixture(&request);
1552        incomplete_drc.drc.value.complete_no_waivers_evaluation = false;
1553        assert_rejected(incomplete_drc, FailureCode::RouteTiming);
1554
1555        let mut waived = fixture(&request);
1556        waived.drc.value.installed_waivers = 1;
1557        assert_rejected(waived, FailureCode::RouteTiming);
1558
1559        let mut error = fixture(&request);
1560        error.drc.value.enabled_error_violations = 1;
1561        assert_rejected(error, FailureCode::RouteTiming);
1562
1563        let mut fatal = fixture(&request);
1564        fatal.drc.value.enabled_fatal_violations = 1;
1565        assert_rejected(fatal, FailureCode::RouteTiming);
1566    }
1567
1568    #[test]
1569    fn kernel_only_relaxed_or_over_limit_resources_fail_closed() {
1570        let request = synthetic_request(&"1".repeat(32));
1571
1572        let mut kernel_only = fixture(&request);
1573        kernel_only.resources.value.scope = ResourceScope::KernelHierarchyOnly;
1574        assert_rejected(kernel_only, FailureCode::RouteResource);
1575
1576        let mut relaxed_cap = fixture(&request);
1577        relaxed_cap.resources.value.declared_dsp_cap = MAXIMUM_DSPS + 1;
1578        assert_rejected(relaxed_cap, FailureCode::RouteResource);
1579
1580        let mut relaxed_lut_cap = fixture(&request);
1581        relaxed_lut_cap.resources.value.declared_lut_cap = MAXIMUM_LUTS + 1;
1582        assert_rejected(relaxed_lut_cap, FailureCode::RouteResource);
1583
1584        let mut relaxed_register_cap = fixture(&request);
1585        relaxed_register_cap.resources.value.declared_register_cap = MAXIMUM_REGISTERS + 1;
1586        assert_rejected(relaxed_register_cap, FailureCode::RouteResource);
1587
1588        let mut excess_lut = fixture(&request);
1589        excess_lut.resources.value.luts = MAXIMUM_LUTS + 1;
1590        assert_rejected(excess_lut, FailureCode::RouteResource);
1591
1592        let mut excess_register = fixture(&request);
1593        excess_register.resources.value.registers = MAXIMUM_REGISTERS + 1;
1594        assert_rejected(excess_register, FailureCode::RouteResource);
1595
1596        let mut excess_dsp = fixture(&request);
1597        excess_dsp.resources.value.dsps = MAXIMUM_DSPS + 1;
1598        assert_rejected(excess_dsp, FailureCode::RouteResource);
1599    }
1600
1601    #[test]
1602    fn artifact_splices_aliases_and_noncanonical_bindings_fail_closed() {
1603        let request = synthetic_request(&"1".repeat(32));
1604
1605        let mut changed_inventory = fixture(&request);
1606        changed_inventory.run_inventory.sha256 = "d".repeat(64);
1607        assert_rejected(changed_inventory, FailureCode::IdentityJoin);
1608
1609        let mut changed_stage = fixture(&request);
1610        changed_stage.stage_manifest.bytes += 1;
1611        assert_rejected(changed_stage, FailureCode::IdentityJoin);
1612
1613        let mut changed_seal = fixture(&request);
1614        changed_seal.route_seal.sha256 = "e".repeat(64);
1615        assert_rejected(changed_seal, FailureCode::IdentityJoin);
1616
1617        let mut inventory_xclbin_splice = fixture(&request);
1618        inventory_xclbin_splice.inventory_xclbin.sha256 = "d".repeat(64);
1619        assert_rejected(inventory_xclbin_splice, FailureCode::IdentityJoin);
1620
1621        let mut inventory_clock_splice = fixture(&request);
1622        inventory_clock_splice
1623            .inventory_routed_clock_evidence
1624            .sha256 = "d".repeat(64);
1625        assert_rejected(inventory_clock_splice, FailureCode::IdentityJoin);
1626
1627        let mut seal_clock_splice = fixture(&request);
1628        seal_clock_splice.seal_routed_clock_evidence.bytes += 1;
1629        assert_rejected(seal_clock_splice, FailureCode::IdentityJoin);
1630
1631        let mut seal_inventory_splice = fixture(&request);
1632        seal_inventory_splice.seal_run_inventory.sha256 = "e".repeat(64);
1633        assert_rejected(seal_inventory_splice, FailureCode::IdentityJoin);
1634
1635        let mut seal_xclbin_splice = fixture(&request);
1636        seal_xclbin_splice.seal_xclbin.bytes += 1;
1637        assert_rejected(seal_xclbin_splice, FailureCode::IdentityJoin);
1638
1639        let mut outside_root = fixture(&request);
1640        outside_root.xclbin_identity.subject.absolute_path = "/other/kernel.xclbin".to_owned();
1641        assert_rejected(outside_root, FailureCode::IdentityJoin);
1642
1643        let mut clock_outside_root = fixture(&request);
1644        clock_outside_root.routed_clock_evidence.absolute_path =
1645            "/other/post-route-evidence.json".to_owned();
1646        assert_rejected(clock_outside_root, FailureCode::IdentityJoin);
1647
1648        let mut alias = fixture(&request);
1649        alias.routed_clock_evidence = xclbin_artifact();
1650        alias.inventory_routed_clock_evidence = xclbin_artifact();
1651        alias.seal_routed_clock_evidence = xclbin_artifact();
1652        assert_rejected(alias, FailureCode::IdentityJoin);
1653
1654        let mut zero_length = fixture(&request);
1655        zero_length.xclbin_identity.subject.bytes = 0;
1656        assert_rejected(zero_length, FailureCode::Noncanonical);
1657
1658        let mut uppercase_digest = fixture(&request);
1659        uppercase_digest.xclbin_identity.subject.sha256 = "A".repeat(64);
1660        assert_rejected(uppercase_digest, FailureCode::Noncanonical);
1661    }
1662
1663    #[test]
1664    fn parser_derived_symlink_realpath_and_hardlink_aliases_fail_closed() {
1665        let request = synthetic_request(&"1".repeat(32));
1666
1667        // A symlinked pathname resolves to a different canonical realpath.
1668        let mut realpath_alias = fixture(&request);
1669        realpath_alias
1670            .routed_clock_evidence
1671            .descriptor
1672            .canonical_realpath = "/evidence/route-acceptance/evidence/other-clock.json".to_owned();
1673        assert_rejected(realpath_alias, FailureCode::IdentityJoin);
1674
1675        let mut hardlink_alias = fixture(&request);
1676        let xclbin_device = hardlink_alias.xclbin_identity.subject.descriptor.device;
1677        let xclbin_inode = hardlink_alias.xclbin_identity.subject.descriptor.inode;
1678        hardlink_alias.inventory_raw_reports[0].descriptor.device = xclbin_device;
1679        hardlink_alias.inventory_raw_reports[0].descriptor.inode = xclbin_inode;
1680        hardlink_alias.seal_raw_reports[0].descriptor.device = xclbin_device;
1681        hardlink_alias.seal_raw_reports[0].descriptor.inode = xclbin_inode;
1682        assert_rejected(hardlink_alias, FailureCode::IdentityJoin);
1683
1684        let mut missing_inode = fixture(&request);
1685        missing_inode.route_seal.descriptor.inode = 0;
1686        assert_rejected(missing_inode, FailureCode::Noncanonical);
1687    }
1688
1689    #[test]
1690    fn raw_report_omissions_splices_ordering_and_aliases_fail_closed() {
1691        let request = synthetic_request(&"1".repeat(32));
1692
1693        let mut empty = fixture(&request);
1694        empty.inventory_raw_reports.clear();
1695        empty.seal_raw_reports.clear();
1696        assert_rejected(empty, FailureCode::RouteUnaccepted);
1697
1698        let mut unsorted = fixture(&request);
1699        unsorted.inventory_raw_reports.swap(0, 1);
1700        unsorted.seal_raw_reports.swap(0, 1);
1701        assert_rejected(unsorted, FailureCode::Noncanonical);
1702
1703        let mut duplicate = fixture(&request);
1704        let duplicate_path = duplicate.inventory_raw_reports[0].absolute_path.clone();
1705        duplicate.inventory_raw_reports[1].absolute_path = duplicate_path.clone();
1706        duplicate.inventory_raw_reports[1]
1707            .descriptor
1708            .canonical_realpath = duplicate_path.clone();
1709        duplicate.seal_raw_reports[1].absolute_path = duplicate_path.clone();
1710        duplicate.seal_raw_reports[1].descriptor.canonical_realpath = duplicate_path;
1711        assert_rejected(duplicate, FailureCode::IdentityJoin);
1712
1713        let mut outside_root = fixture(&request);
1714        outside_root.inventory_raw_reports[0].absolute_path = "/other/report-a.raw".to_owned();
1715        outside_root.inventory_raw_reports[0]
1716            .descriptor
1717            .canonical_realpath = "/other/report-a.raw".to_owned();
1718        outside_root.seal_raw_reports[0].absolute_path = "/other/report-a.raw".to_owned();
1719        outside_root.seal_raw_reports[0]
1720            .descriptor
1721            .canonical_realpath = "/other/report-a.raw".to_owned();
1722        assert_rejected(outside_root, FailureCode::IdentityJoin);
1723
1724        let mut seal_digest_splice = fixture(&request);
1725        seal_digest_splice.seal_raw_reports[0].sha256 = "f".repeat(64);
1726        assert_rejected(seal_digest_splice, FailureCode::IdentityJoin);
1727
1728        let mut primary_alias = fixture(&request);
1729        primary_alias.inventory_raw_reports[0] = xclbin_artifact();
1730        primary_alias.seal_raw_reports[0] = xclbin_artifact();
1731        assert_rejected(primary_alias, FailureCode::IdentityJoin);
1732
1733        let mut zero_length = fixture(&request);
1734        zero_length.inventory_raw_reports[0].bytes = 0;
1735        zero_length.seal_raw_reports[0].bytes = 0;
1736        assert_rejected(zero_length, FailureCode::Noncanonical);
1737    }
1738
1739    #[test]
1740    fn xclbin_identity_mutations_fail_closed() {
1741        let request = synthetic_request(&"1".repeat(32));
1742
1743        let mut uppercase_uuid = fixture(&request);
1744        uppercase_uuid.xclbin_identity.uuid = XCLBIN_UUID.to_ascii_uppercase();
1745        assert_rejected(uppercase_uuid, FailureCode::Noncanonical);
1746
1747        let mut canonical_uuid_substitution = fixture(&request);
1748        canonical_uuid_substitution.xclbin_identity.uuid =
1749            "22222222-3333-4444-5555-666666666666".to_owned();
1750        assert_rejected(canonical_uuid_substitution, FailureCode::IdentityJoin);
1751
1752        let mut zero_interface_uuid = fixture(&request);
1753        zero_interface_uuid.xclbin_identity.interface_uuid =
1754            "00000000-0000-0000-0000-000000000000".to_owned();
1755        assert_rejected(zero_interface_uuid, FailureCode::Noncanonical);
1756
1757        let mut canonical_interface_substitution = fixture(&request);
1758        canonical_interface_substitution
1759            .xclbin_identity
1760            .interface_uuid = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff".to_owned();
1761        assert_rejected(canonical_interface_substitution, FailureCode::IdentityJoin);
1762
1763        let mut wrong_platform = fixture(&request);
1764        wrong_platform.xclbin_identity.deployment_platform = "other-platform".to_owned();
1765        assert_rejected(wrong_platform, FailureCode::IdentityJoin);
1766
1767        let mut wrong_part = fixture(&request);
1768        wrong_part.xclbin_identity.fpga_part = "other-part".to_owned();
1769        assert_rejected(wrong_part, FailureCode::IdentityJoin);
1770
1771        let mut wrong_target = fixture(&request);
1772        wrong_target.xclbin_identity.target = "hw_emu".to_owned();
1773        assert_rejected(wrong_target, FailureCode::RouteUnaccepted);
1774
1775        let mut subject_splice = fixture(&request);
1776        subject_splice.xclbin_identity = RouteXclbinIdentity::from_exact_parser(
1777            subject(
1778                "/evidence/route-acceptance/output/other-kernel.xclbin",
1779                1_001,
1780                "f".repeat(64),
1781                41,
1782            ),
1783            XCLBIN_UUID.to_owned(),
1784            INTERFACE_UUID.to_owned(),
1785            REQUIRED_PLATFORM.to_owned(),
1786            REQUIRED_DEVICE_PART.to_owned(),
1787            REQUIRED_XCLBIN_TARGET.to_owned(),
1788        )
1789        .unwrap();
1790        assert_rejected(subject_splice, FailureCode::IdentityJoin);
1791    }
1792
1793    #[test]
1794    fn route_tier_cannot_prepopulate_any_terminal_claim() {
1795        let request = synthetic_request(&"1".repeat(32));
1796
1797        let mut hardware = fixture(&request);
1798        hardware.nonclaims.hardware_execution = true;
1799        assert_rejected(hardware, FailureCode::PrematureClaim);
1800
1801        let mut card = fixture(&request);
1802        card.nonclaims.card_validated = true;
1803        assert_rejected(card, FailureCode::PrematureClaim);
1804
1805        let mut crypto = fixture(&request);
1806        crypto.nonclaims.cryptographic_outputs_valid = true;
1807        assert_rejected(crypto, FailureCode::PrematureClaim);
1808
1809        let mut performance = fixture(&request);
1810        performance.nonclaims.performance_validated = true;
1811        assert_rejected(performance, FailureCode::PrematureClaim);
1812
1813        let mut completion = fixture(&request);
1814        completion.nonclaims.hardware_completion_promotable = true;
1815        assert_rejected(completion, FailureCode::PrematureClaim);
1816    }
1817}