PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
nm_iod_validator.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <sstream>
15
17
18using namespace kcenon::pacs::core;
19
20// NM-specific DICOM tags
21namespace nm_tags {
22 // NM Image Module
23 constexpr dicom_tag image_type{0x0008, 0x0008};
24 constexpr dicom_tag number_of_frames{0x0028, 0x0008};
25 constexpr dicom_tag frame_increment_pointer{0x0028, 0x0009};
26
27 // NM Isotope Module
28 constexpr dicom_tag energy_window_info_sequence{0x0054, 0x0012};
29 constexpr dicom_tag energy_window_range_sequence{0x0054, 0x0013};
31 constexpr dicom_tag radionuclide_total_dose{0x0018, 0x1074};
32 constexpr dicom_tag radionuclide_half_life{0x0018, 0x1075};
33 constexpr dicom_tag radiopharmaceutical_start_time{0x0018, 0x1072};
34
35 // NM Detector Module
36 constexpr dicom_tag detector_info_sequence{0x0054, 0x0022};
37 constexpr dicom_tag collimator_type{0x0018, 0x1181};
38
39 // NM TOMO Acquisition Module (SPECT)
40 constexpr dicom_tag rotation_info_sequence{0x0054, 0x0052};
41 constexpr dicom_tag type_of_data{0x0054, 0x0400};
42 constexpr dicom_tag start_angle{0x0054, 0x0200};
43 constexpr dicom_tag scan_arc{0x0018, 0x1144};
44 constexpr dicom_tag rotation_direction{0x0018, 0x1140};
45
46 // NM Multi-gated Acquisition Module
47 constexpr dicom_tag gated_info_sequence{0x0054, 0x0062};
48 constexpr dicom_tag trigger_source_or_type{0x0018, 0x1061};
49 constexpr dicom_tag number_of_time_slots{0x0054, 0x0071};
50}
51
52// =============================================================================
53// nm_iod_validator Implementation
54// =============================================================================
55
57 : options_(options) {}
58
60 validation_result result;
61 result.is_valid = true;
62
63 // Validate mandatory modules
65 validate_patient_module(dataset, result.findings);
68 validate_nm_series_module(dataset, result.findings);
69 validate_nm_image_module(dataset, result.findings);
70 validate_sop_common_module(dataset, result.findings);
71 }
72
75 }
76
77 // NM-specific validation
81 }
83 validate_nm_isotope_module(dataset, result.findings);
85 }
87 }
88
89 // Check for errors
90 for (const auto& finding : result.findings) {
91 if (finding.severity == validation_severity::error) {
92 result.is_valid = false;
93 break;
94 }
95 if (options_.strict_mode && finding.severity == validation_severity::warning) {
96 result.is_valid = false;
97 break;
98 }
99 }
100
101 return result;
102}
103
106 // First do standard validation
107 auto result = validate(dataset);
108
109 // Then add multi-frame specific validation
110 validate_multiframe_module(dataset, result.findings);
111
112 // Validate SPECT-specific if applicable
113 if (dataset.contains(nm_tags::type_of_data)) {
114 auto type = dataset.get_string(nm_tags::type_of_data);
115 if (type.find("TOMO") != std::string::npos) {
116 validate_nm_tomo_module(dataset, result.findings);
117 }
118 if (type.find("GATED") != std::string::npos) {
119 validate_nm_gated_module(dataset, result.findings);
120 }
121 }
122
123 // Re-check validity after multi-frame validation
124 for (const auto& finding : result.findings) {
125 if (finding.severity == validation_severity::error) {
126 result.is_valid = false;
127 break;
128 }
129 }
130
131 return result;
132}
133
135 // Check only Type 1 required attributes for quick validation
136
137 // General Study Module Type 1
138 if (!dataset.contains(tags::study_instance_uid)) return false;
139
140 // General Series Module Type 1
141 if (!dataset.contains(tags::modality)) return false;
142 if (!dataset.contains(tags::series_instance_uid)) return false;
143
144 // Check modality is NM
145 auto modality = dataset.get_string(tags::modality);
146 if (modality != "NM") return false;
147
148 // Image Pixel Module Type 1
149 if (!dataset.contains(tags::samples_per_pixel)) return false;
150 if (!dataset.contains(tags::photometric_interpretation)) return false;
151 if (!dataset.contains(tags::rows)) return false;
152 if (!dataset.contains(tags::columns)) return false;
153 if (!dataset.contains(tags::bits_allocated)) return false;
154 if (!dataset.contains(tags::bits_stored)) return false;
155 if (!dataset.contains(tags::high_bit)) return false;
156 if (!dataset.contains(tags::pixel_representation)) return false;
157 if (!dataset.contains(tags::pixel_data)) return false;
158
159 // SOP Common Module Type 1
160 if (!dataset.contains(tags::sop_class_uid)) return false;
161 if (!dataset.contains(tags::sop_instance_uid)) return false;
162
163 return true;
164}
165
167 return options_;
168}
169
173
174// =============================================================================
175// Module Validation Methods
176// =============================================================================
177
179 const dicom_dataset& dataset,
180 std::vector<validation_finding>& findings) const {
181
182 // Patient Module - All attributes are Type 2
183 if (options_.check_type2) {
184 check_type2_attribute(dataset, tags::patient_name, "PatientName", findings);
185 check_type2_attribute(dataset, tags::patient_id, "PatientID", findings);
186 check_type2_attribute(dataset, tags::patient_birth_date, "PatientBirthDate", findings);
187 check_type2_attribute(dataset, tags::patient_sex, "PatientSex", findings);
188 }
189}
190
192 const dicom_dataset& dataset,
193 std::vector<validation_finding>& findings) const {
194
195 // Type 1
196 if (options_.check_type1) {
197 check_type1_attribute(dataset, tags::study_instance_uid, "StudyInstanceUID", findings);
198 }
199
200 // Type 2
201 if (options_.check_type2) {
202 check_type2_attribute(dataset, tags::study_date, "StudyDate", findings);
203 check_type2_attribute(dataset, tags::study_time, "StudyTime", findings);
204 check_type2_attribute(dataset, tags::referring_physician_name, "ReferringPhysicianName", findings);
205 check_type2_attribute(dataset, tags::study_id, "StudyID", findings);
206 check_type2_attribute(dataset, tags::accession_number, "AccessionNumber", findings);
207 }
208}
209
211 const dicom_dataset& dataset,
212 std::vector<validation_finding>& findings) const {
213
214 // Type 1
215 if (options_.check_type1) {
216 check_type1_attribute(dataset, tags::modality, "Modality", findings);
217 check_type1_attribute(dataset, tags::series_instance_uid, "SeriesInstanceUID", findings);
218
219 // Special check: Modality must be "NM"
220 check_modality(dataset, findings);
221 }
222
223 // Type 2
224 if (options_.check_type2) {
225 check_type2_attribute(dataset, tags::series_number, "SeriesNumber", findings);
226 }
227}
228
230 const dicom_dataset& dataset,
231 std::vector<validation_finding>& findings) const {
232
233 // Type of Data is important for NM series classification
234 if (options_.check_type1) {
235 check_type1_attribute(dataset, nm_tags::type_of_data, "TypeOfData", findings);
236 }
237
238 // Validate TypeOfData value
239 if (dataset.contains(nm_tags::type_of_data)) {
240 auto type = dataset.get_string(nm_tags::type_of_data);
241 // Valid values: STATIC, DYNAMIC, GATED, WHOLE BODY, TOMO, GATED TOMO, RECON TOMO, RECON GATED TOMO
242 if (type != "STATIC" && type != "DYNAMIC" && type != "GATED" &&
243 type != "WHOLE BODY" && type != "TOMO" && type != "GATED TOMO" &&
244 type != "RECON TOMO" && type != "RECON GATED TOMO") {
245 findings.push_back({
248 "Unusual TypeOfData value for NM: " + type,
249 "NM-WARN-001"
250 });
251 }
252 }
253}
254
256 const dicom_dataset& dataset,
257 std::vector<validation_finding>& findings) const {
258
259 // Type 1
260 if (options_.check_type1) {
261 check_type1_attribute(dataset, nm_tags::image_type, "ImageType", findings);
262 }
263
264 // NumberOfFrames is Type 1 for NM (often multi-frame)
265 if (options_.check_type1) {
266 check_type1_attribute(dataset, nm_tags::number_of_frames, "NumberOfFrames", findings);
267 }
268
269 // Validate ImageType format
270 if (dataset.contains(nm_tags::image_type)) {
271 auto image_type = dataset.get_string(nm_tags::image_type);
272 if (image_type.find('\\') == std::string::npos) {
273 findings.push_back({
276 "ImageType should contain backslash separators",
277 "NM-INFO-001"
278 });
279 }
280 }
281}
282
284 const dicom_dataset& dataset,
285 std::vector<validation_finding>& findings) const {
286
287 // Energy Window Information Sequence is Type 2 per DICOM standard
288 // (Required, but may be empty)
289 if (options_.check_type2) {
291 "EnergyWindowInformationSequence", findings);
292 }
293
294 // Radiopharmaceutical Information Sequence is Type 2
295 if (options_.check_type2) {
297 "RadiopharmaceuticalInformationSequence", findings);
298 }
299}
300
302 const dicom_dataset& dataset,
303 std::vector<validation_finding>& findings) const {
304
305 // Detector Information Sequence is Type 2
306 if (options_.check_type2) {
308 "DetectorInformationSequence", findings);
309 }
310
311 // Collimator Type is informational but important
312 if (!dataset.contains(nm_tags::collimator_type)) {
313 findings.push_back({
316 "CollimatorType not specified - helps with interpretation",
317 "NM-INFO-002"
318 });
319 } else {
320 auto collimator = dataset.get_string(nm_tags::collimator_type);
321 // Validate collimator type
322 if (collimator != "PARA" && collimator != "FANB" && collimator != "CONE" &&
323 collimator != "PINH" && collimator != "DIVG" && collimator != "NONE") {
324 findings.push_back({
327 "Non-standard CollimatorType: " + collimator,
328 "NM-WARN-002"
329 });
330 }
331 }
332}
333
335 const dicom_dataset& dataset,
336 std::vector<validation_finding>& findings) const {
337
338 // Rotation Information Sequence is Type 2 for SPECT
339 if (options_.check_type2) {
341 "RotationInformationSequence", findings);
342 }
343
344 // Start Angle and Scan Arc are important for SPECT reconstruction
345 if (!dataset.contains(nm_tags::start_angle)) {
346 findings.push_back({
349 "StartAngle not specified for SPECT acquisition",
350 "NM-WARN-003"
351 });
352 }
353
354 if (!dataset.contains(nm_tags::scan_arc)) {
355 findings.push_back({
358 "ScanArc not specified for SPECT acquisition",
359 "NM-WARN-004"
360 });
361 }
362
363 // Rotation Direction
365 auto direction = dataset.get_string(nm_tags::rotation_direction);
366 if (direction != "CW" && direction != "CC") {
367 findings.push_back({
370 "Invalid RotationDirection: " + direction + " (expected CW or CC)",
371 "NM-WARN-005"
372 });
373 }
374 }
375}
376
378 const dicom_dataset& dataset,
379 std::vector<validation_finding>& findings) const {
380
381 // Gated Information Sequence is Type 2 for gated acquisitions
382 if (options_.check_type2) {
384 "GatedInformationSequence", findings);
385 }
386
387 // Trigger Source for gated acquisition
389 findings.push_back({
392 "TriggerSourceOrType not specified for gated acquisition",
393 "NM-INFO-003"
394 });
395 }
396
397 // Number of Time Slots is important for gated analysis
399 findings.push_back({
402 "NumberOfTimeSlots not specified for gated acquisition",
403 "NM-WARN-006"
404 });
405 }
406}
407
409 const dicom_dataset& dataset,
410 std::vector<validation_finding>& findings) const {
411
412 // Type 1 attributes
413 if (options_.check_type1) {
414 check_type1_attribute(dataset, tags::samples_per_pixel, "SamplesPerPixel", findings);
416 "PhotometricInterpretation", findings);
417 check_type1_attribute(dataset, tags::rows, "Rows", findings);
418 check_type1_attribute(dataset, tags::columns, "Columns", findings);
419 check_type1_attribute(dataset, tags::bits_allocated, "BitsAllocated", findings);
420 check_type1_attribute(dataset, tags::bits_stored, "BitsStored", findings);
421 check_type1_attribute(dataset, tags::high_bit, "HighBit", findings);
422 check_type1_attribute(dataset, tags::pixel_representation, "PixelRepresentation", findings);
423 check_type1_attribute(dataset, tags::pixel_data, "PixelData", findings);
424 }
425
426 // Validate pixel data consistency
428 check_pixel_data_consistency(dataset, findings);
429 }
430}
431
433 const dicom_dataset& dataset,
434 std::vector<validation_finding>& findings) const {
435
436 // NumberOfFrames should be present for multi-frame NM
437 if (options_.check_type1) {
438 check_type1_attribute(dataset, nm_tags::number_of_frames, "NumberOfFrames", findings);
439 }
440
441 // FrameIncrementPointer is Type 1C for multi-frame
443 auto num_frames = dataset.get_numeric<int32_t>(nm_tags::number_of_frames);
444 if (num_frames && *num_frames > 1) {
446 findings.push_back({
449 "FrameIncrementPointer recommended for multi-frame NM images",
450 "NM-WARN-007"
451 });
452 }
453 }
454 }
455}
456
458 const dicom_dataset& dataset,
459 std::vector<validation_finding>& findings) const {
460
461 // Type 1
462 if (options_.check_type1) {
463 check_type1_attribute(dataset, tags::sop_class_uid, "SOPClassUID", findings);
464 check_type1_attribute(dataset, tags::sop_instance_uid, "SOPInstanceUID", findings);
465 }
466
467 // Validate SOP Class UID is a NM storage class
468 if (dataset.contains(tags::sop_class_uid)) {
469 auto sop_class = dataset.get_string(tags::sop_class_uid);
470 if (!sop_classes::is_nm_storage_sop_class(sop_class)) {
471 findings.push_back({
474 "SOPClassUID is not a recognized NM Storage SOP Class: " + sop_class,
475 "NM-ERR-001"
476 });
477 }
478 }
479}
480
481// =============================================================================
482// NM-Specific Validation
483// =============================================================================
484
486 const dicom_dataset& dataset,
487 std::vector<validation_finding>& findings) const {
488
489 // Energy Window Information Sequence is Type 1
491 // Already reported as Type 1 missing
492 return;
493 }
494
495 // Energy Window Range Sequence should be present
497 findings.push_back({
500 "EnergyWindowRangeSequence not present - energy window details missing",
501 "NM-WARN-008"
502 });
503 }
504}
505
507 const dicom_dataset& dataset,
508 std::vector<validation_finding>& findings) const {
509
510 // If sequence is present, validate its contents
512 // Radionuclide Total Dose is important for quantitative analysis
514 findings.push_back({
517 "RadionuclideTotalDose not specified",
518 "NM-INFO-004"
519 });
520 }
521
522 // Radionuclide Half Life is important for decay correction
524 findings.push_back({
527 "RadionuclideHalfLife not specified",
528 "NM-INFO-005"
529 });
530 }
531
532 // Radiopharmaceutical Start Time for timing calculations
534 findings.push_back({
537 "RadiopharmaceuticalStartTime not specified",
538 "NM-INFO-006"
539 });
540 }
541 }
542}
543
544// =============================================================================
545// Attribute Validation Helpers
546// =============================================================================
547
549 const dicom_dataset& dataset,
550 dicom_tag tag,
551 std::string_view name,
552 std::vector<validation_finding>& findings) const {
553
554 if (!dataset.contains(tag)) {
555 findings.push_back({
557 tag,
558 std::string("Type 1 attribute missing: ") + std::string(name) +
559 " (" + tag.to_string() + ")",
560 "NM-TYPE1-MISSING"
561 });
562 } else {
563 // Type 1 must have a value (cannot be empty)
564 auto value = dataset.get_string(tag);
565 if (value.empty()) {
566 findings.push_back({
568 tag,
569 std::string("Type 1 attribute has empty value: ") +
570 std::string(name) + " (" + tag.to_string() + ")",
571 "NM-TYPE1-EMPTY"
572 });
573 }
574 }
575}
576
578 const dicom_dataset& dataset,
579 dicom_tag tag,
580 std::string_view name,
581 std::vector<validation_finding>& findings) const {
582
583 // Type 2 must be present but can be empty
584 if (!dataset.contains(tag)) {
585 findings.push_back({
587 tag,
588 std::string("Type 2 attribute missing: ") + std::string(name) +
589 " (" + tag.to_string() + ")",
590 "NM-TYPE2-MISSING"
591 });
592 }
593}
594
596 const dicom_dataset& dataset,
597 std::vector<validation_finding>& findings) const {
598
599 if (!dataset.contains(tags::modality)) {
600 return; // Already reported as Type 1 missing
601 }
602
603 auto modality = dataset.get_string(tags::modality);
604 if (modality != "NM") {
605 findings.push_back({
608 "Modality must be 'NM' for nuclear medicine images, found: " + modality,
609 "NM-ERR-002"
610 });
611 }
612}
613
615 const dicom_dataset& dataset,
616 std::vector<validation_finding>& findings) const {
617
618 // Check BitsStored <= BitsAllocated
619 auto bits_allocated = dataset.get_numeric<uint16_t>(tags::bits_allocated);
620 auto bits_stored = dataset.get_numeric<uint16_t>(tags::bits_stored);
621 auto high_bit = dataset.get_numeric<uint16_t>(tags::high_bit);
622
623 if (bits_allocated && bits_stored) {
624 if (*bits_stored > *bits_allocated) {
625 findings.push_back({
628 "BitsStored (" + std::to_string(*bits_stored) +
629 ") exceeds BitsAllocated (" + std::to_string(*bits_allocated) + ")",
630 "NM-ERR-003"
631 });
632 }
633 }
634
635 // Check HighBit == BitsStored - 1
636 if (bits_stored && high_bit) {
637 if (*high_bit != *bits_stored - 1) {
638 findings.push_back({
641 "HighBit (" + std::to_string(*high_bit) +
642 ") should typically be BitsStored - 1 (" +
643 std::to_string(*bits_stored - 1) + ")",
644 "NM-WARN-009"
645 });
646 }
647 }
648
649 // Check photometric interpretation is valid for NM
651 auto photometric = dataset.get_string(tags::photometric_interpretation);
652 if (!sop_classes::is_valid_nm_photometric(photometric)) {
653 findings.push_back({
656 "Unusual photometric interpretation for NM: " + photometric +
657 " (expected MONOCHROME2 or PALETTE COLOR)",
658 "NM-WARN-010"
659 });
660 }
661 }
662
663 // NM images should be grayscale (SamplesPerPixel = 1)
664 auto samples = dataset.get_numeric<uint16_t>(tags::samples_per_pixel);
665 if (samples && *samples != 1) {
666 findings.push_back({
669 "NM images should have SamplesPerPixel = 1, found: " +
670 std::to_string(*samples),
671 "NM-WARN-011"
672 });
673 }
674}
675
676// =============================================================================
677// Convenience Functions
678// =============================================================================
679
681 nm_iod_validator validator;
682 return validator.validate(dataset);
683}
684
685bool is_valid_nm_dataset(const dicom_dataset& dataset) {
686 nm_iod_validator validator;
687 return validator.quick_check(dataset);
688}
689
690} // namespace kcenon::pacs::services::validation
auto get_numeric(dicom_tag tag) const -> std::optional< T >
Get the numeric value of an element.
auto contains(dicom_tag tag) const noexcept -> bool
Check if the dataset contains an element with the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
auto to_string() const -> std::string
Convert to string representation.
void validate_energy_window_info(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_general_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
validation_result validate_multiframe(const core::dicom_dataset &dataset) const
Validate a multi-frame NM dataset.
void check_pixel_data_consistency(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type1_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
nm_iod_validator()=default
Construct validator with default options.
validation_result validate(const core::dicom_dataset &dataset) const
Validate a DICOM dataset against NM IOD.
void validate_general_study_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void set_options(const nm_validation_options &options)
Set validation options.
void validate_multiframe_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_isotope_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_tomo_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_gated_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_detector_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
const nm_validation_options & options() const noexcept
Get the validation options.
void validate_patient_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_series_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
bool quick_check(const core::dicom_dataset &dataset) const
Quick check if dataset has minimum required attributes.
void validate_image_pixel_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_modality(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_nm_image_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void validate_radiopharmaceutical_info(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
void check_type2_attribute(const core::dicom_dataset &dataset, core::dicom_tag tag, std::string_view name, std::vector< validation_finding > &findings) const
void validate_sop_common_module(const core::dicom_dataset &dataset, std::vector< validation_finding > &findings) const
Compile-time constants for commonly used DICOM tags.
constexpr dicom_tag high_bit
High Bit.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag bits_allocated
Bits Allocated.
constexpr dicom_tag rows
Rows.
constexpr dicom_tag columns
Columns.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag bits_stored
Bits Stored.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag pixel_data
Pixel Data.
constexpr dicom_tag pixel_representation
Pixel Representation.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag samples_per_pixel
Samples per Pixel.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag photometric_interpretation
Photometric Interpretation.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
bool is_valid_nm_photometric(std::string_view value) noexcept
Check if photometric interpretation is valid for NM.
bool is_nm_storage_sop_class(std::string_view uid) noexcept
Check if a SOP Class UID is a NM Storage SOP Class.
bool is_valid_nm_dataset(const core::dicom_dataset &dataset)
Quick check if a dataset is a valid NM image.
@ warning
Non-critical - IOD may have issues.
@ info
Informational - suggestion for improvement.
validation_result validate_nm_iod(const core::dicom_dataset &dataset)
Validate a NM dataset with default options.
Nuclear Medicine (NM) Image IOD Validator.
Nuclear Medicine (NM) Image Storage SOP Classes.
bool validate_pixel_data
Validate pixel data consistency (rows, columns, bits)
bool validate_energy_windows
Validate energy window information.
bool validate_nm_specific
Validate NM-specific attributes (detector, collimator, etc.)
bool check_type2
Check Type 2 (required, can be empty) attributes.
bool check_type1
Check Type 1 (required) attributes.
bool check_conditional
Check Type 1C/2C (conditionally required) attributes.
bool strict_mode
Strict mode - treat warnings as errors.
std::vector< validation_finding > findings
All findings during validation.
std::string_view name