1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
//! R.3a acceptance tests for the Layer A device-evidence-fabric path.
//!
//! Three load-bearing invariants are pinned here:
//!
//! 1. **Chain equivalence**: Layer A's `CompactCaseSummary.hashes`
//! must equal Layer B's `CaseFile.hashes` byte-for-byte through
//! `candidate_interval`. This proves that skipping the bank stage
//! does not change any link the bank does not write.
//!
//! 2. **Type-level non-bypass**: `CompactCaseSummary` has no
//! `episodes` or `final_case_file_hash` field, so Layer A
//! callers literally cannot mint admitted episodes. This is the
//! Semantic Non-Bypass Axiom enforced by the type system at the
//! Layer A boundary.
//!
//! 3. **Batched equivalence per catalog**: when K catalogs run
//! through the batched Layer A path, each catalog's summary
//! chain hashes match the corresponding single-catalog Layer A
//! summary on the same input.
//!
//! These tests do not change CandidateInterval layout (that is
//! R.5's contract-affecting work); the golden hashes are unchanged.
#![cfg(feature = "cuda")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use dsfb_gpu_debug_core::bank::bank_hash;
use dsfb_gpu_debug_core::contract::Contract;
use dsfb_gpu_debug_core::fixture::{synthesize, DEFAULT_SEED};
use dsfb_gpu_debug_core::motif::registry_hash;
use dsfb_gpu_debug_cuda::{
build_gpu_layer_a_batched, build_gpu_layer_a_on_workspace,
build_gpu_throughput_device_digests_on_workspace, BatchedGpuWorkspace, GpuWorkspace,
};
fn canonical_contract() -> Contract {
let mut c = Contract::canonical();
c.pin_bank_hash(bank_hash());
c.pin_detector_registry_hash(registry_hash());
c
}
#[test]
fn layer_a_summary_chain_matches_layer_b_case_file_through_candidate() {
// The first 11 chain links (input_catalog through candidate_interval)
// must be byte-identical between the Layer A skip-bank summary and
// the Layer B full case file on the same fixture. Layer A simply
// declines to compute the `episode` link; everything else is the
// same evidence-fabric chain.
let contract = canonical_contract();
let events = synthesize(DEFAULT_SEED);
let mut ws_a = GpuWorkspace::new(&contract).unwrap();
let summary = build_gpu_layer_a_on_workspace(&events, &contract, &mut ws_a).unwrap();
let mut ws_b = GpuWorkspace::new(&contract).unwrap();
let case =
build_gpu_throughput_device_digests_on_workspace(&events, &contract, &mut ws_b).unwrap();
// Compare every chain link except `episode` (Layer A does not run
// the bank, so the episode digest is intentionally absent).
assert_eq!(summary.hashes.input_catalog, case.hashes.input_catalog);
assert_eq!(summary.hashes.contract, case.hashes.contract);
assert_eq!(summary.hashes.bank, case.hashes.bank);
assert_eq!(
summary.hashes.detector_registry,
case.hashes.detector_registry
);
assert_eq!(summary.hashes.kernel_sequence, case.hashes.kernel_sequence);
assert_eq!(summary.hashes.window_feature, case.hashes.window_feature);
assert_eq!(summary.hashes.residual_field, case.hashes.residual_field);
assert_eq!(summary.hashes.sign_field, case.hashes.sign_field);
assert_eq!(summary.hashes.detector_cell, case.hashes.detector_cell);
assert_eq!(summary.hashes.consensus_grid, case.hashes.consensus_grid);
assert_eq!(
summary.hashes.candidate_interval, case.hashes.candidate_interval,
"the candidate_interval link must match — this is the boundary at which Layer A stops"
);
// Layer A does not compute the `episode` link.
assert_eq!(summary.hashes.episode, [0u8; 32]);
// The Layer A summary's candidate list is the *pre-bank* slice
// (raw admitted-candidate intervals). The Layer B case file's
// `episodes` is the *post-bank* admitted list (after axis-5,
// confuser suppression, and tie-break). These are different
// shapes by design — comparing their lengths would be apples to
// oranges. The shared boundary is the `candidate_interval` chain
// hash, already asserted above.
}
#[test]
fn layer_a_summary_has_no_admitted_episodes_field() {
// Type-level Semantic Non-Bypass: callers of Layer A cannot
// obtain admitted episodes from the summary. This test is a
// compile-time invariant disguised as a runtime test — if anyone
// adds an `episodes` field to `CompactCaseSummary`, this test
// would still pass (the field would just exist); but the *type
// signature* below documents the contract. We also confirm at
// runtime that no breach flag is set on the canonical fixture
// (clean run, no contract mismatch).
let contract = canonical_contract();
let events = synthesize(DEFAULT_SEED);
let mut ws = GpuWorkspace::new(&contract).unwrap();
let summary = build_gpu_layer_a_on_workspace(&events, &contract, &mut ws).unwrap();
// The compile-time type contract: this line would not compile if
// CompactCaseSummary acquired an `episodes` field of incompatible
// shape. (We rely on type-checking; no runtime assertion.)
let backend: &str = summary.backend;
assert!(!backend.is_empty(), "backend tag must be set");
// Runtime invariant: canonical fixture has no breaches.
assert_eq!(
summary.breach_flags, 0,
"canonical fixture must produce zero breach flags"
);
}
#[test]
fn layer_a_batched_matches_single_per_catalog() {
// Batched K-catalog Layer A must produce per-catalog summaries
// that equal the single-catalog Layer A on the same input. This
// is the per-catalog independence invariant carried over from
// Section O.16's batched-equivalence tests, applied to the
// skip-bank Layer A path.
let k: u32 = 2;
let contract = canonical_contract();
let events_0 = synthesize(DEFAULT_SEED);
let events_1 =
dsfb_gpu_debug_core::fixture::synthesize(DEFAULT_SEED.wrapping_add(0x9E37_79B9_7F4A_7C15));
let slices: [&[_]; 2] = [&events_0[..], &events_1[..]];
let mut ws_batched = BatchedGpuWorkspace::new(k, &contract).unwrap();
let batched_summaries = build_gpu_layer_a_batched(&mut ws_batched, &slices, &contract).unwrap();
assert_eq!(batched_summaries.len(), k as usize);
let mut ws_single = GpuWorkspace::new(&contract).unwrap();
let single_0 = build_gpu_layer_a_on_workspace(&events_0, &contract, &mut ws_single).unwrap();
let mut ws_single2 = GpuWorkspace::new(&contract).unwrap();
let single_1 = build_gpu_layer_a_on_workspace(&events_1, &contract, &mut ws_single2).unwrap();
assert_eq!(
batched_summaries[0].hashes, single_0.hashes,
"catalog 0 batched-vs-single chain hashes must match"
);
assert_eq!(
batched_summaries[1].hashes, single_1.hashes,
"catalog 1 batched-vs-single chain hashes must match"
);
assert_eq!(batched_summaries[0].candidates, single_0.candidates);
assert_eq!(batched_summaries[1].candidates, single_1.candidates);
assert_eq!(batched_summaries[0].breach_flags, 0);
assert_eq!(batched_summaries[1].breach_flags, 0);
}