From da475d74b51b5248096c49373cc66ec7059e21bf Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Sun, 11 Jan 2026 12:52:37 +0100 Subject: [PATCH 1/5] Improve rule validation logging to handle version mismatches gracefully --- scripts/script_utils.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/scripts/script_utils.py b/scripts/script_utils.py index 2fc044616..ed73b2226 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -275,26 +275,31 @@ def load_specified_rules( not excluded_rule_ids or rule not in excluded_rule_ids ): valid_rule_ids.add(rule) - # Check that all specified rules are valid + + # Log and skip any explicitly included rules that are not in the standard if rule_ids: for rule in rule_ids: if rule not in standard_rules: - raise ValueError( - f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}" + engine_logger.error( + f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}. " + "It will be skipped from validation." ) else: for rule in excluded_rule_ids: if rule not in standard_rules: - raise ValueError( - f"The rule specified to exclude '{rule}' is not in the standard {standard} and version {version}" + engine_logger.error( + f"The rule specified to exclude '{rule}' is not in the standard {standard} and version {version}. " + "It is not present and will be ignored." ) + rules = [] for rule_id in valid_rule_ids: rule_data = rules_data.get(rule_id) rules.append(rule_data) - # If no valid rules were found, raise an error + # If no valid rules were found, log an error but do not raise to avoid + # failing the whole run; the caller will receive an empty rules list. if not rules: - raise ValueError( + engine_logger.error( f"All specified rules were excluded because they are not in the standard {standard} and version {version}" ) return rules From b2081804567558f5d4f008c8999ba9dea8ac0f62 Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Mon, 12 Jan 2026 16:03:13 +0100 Subject: [PATCH 2/5] Enhance rule validation to handle skipped rules and improve logging --- .../models/rule_validation_result.py | 17 ++++++++++++ scripts/run_validation.py | 12 +++++++-- scripts/script_utils.py | 27 +++++++++++++------ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/cdisc_rules_engine/models/rule_validation_result.py b/cdisc_rules_engine/models/rule_validation_result.py index 033aef4c4..01c6ca856 100644 --- a/cdisc_rules_engine/models/rule_validation_result.py +++ b/cdisc_rules_engine/models/rule_validation_result.py @@ -4,6 +4,7 @@ from cdisc_rules_engine.interfaces import RepresentationInterface from cdisc_rules_engine.utilities.utils import get_execution_status from cdisc_rules_engine.models.rule import Rule +from cdisc_rules_engine.enums.execution_status import ExecutionStatus @dataclass @@ -27,6 +28,22 @@ def __init__(self, rule: Rule, results: List[dict | str]): self.execution_status = get_execution_status(results) self.results = results + @classmethod + def from_skipped_rule( + cls, + rule_id: str, + message: str | None = None, + ) -> "RuleValidationResult": + instance = cls.__new__(cls) + instance.id = rule_id + instance.cdisc_rule_id = None + instance.fda_rule_id = None + instance.executability = None + instance.message = message + instance.execution_status = ExecutionStatus.SKIPPED.value + instance.results = [] + return instance + def _get_rule_ids(self, rule: Rule, org: str) -> str: return ", ".join( sorted( diff --git a/scripts/run_validation.py b/scripts/run_validation.py index ba5cddf26..3c2bc555a 100644 --- a/scripts/run_validation.py +++ b/scripts/run_validation.py @@ -129,9 +129,10 @@ def run_validation(args: Validation_args): manager = CacheManager() manager.start() try: + created_files = [] shared_cache = get_cache_service(manager) engine_logger.info(f"Populating cache, cache path: {args.cache}") - rules = get_rules(args) + rules, skipped_rule_ids = get_rules(args) library_metadata: LibraryMetadataContainer = get_library_metadata_from_cache( args ) @@ -156,7 +157,6 @@ def run_validation(args: Validation_args): data_service.dataset_implementation != PandasDataset ) datasets = data_service.get_datasets() - created_files = [] if large_dataset_validation and data_service.standard != "usdm": # convert all files to parquet temp files engine_logger.warning( @@ -191,6 +191,14 @@ def run_validation(args: Validation_args): progress_handler: Callable = get_progress_displayer(args) results = progress_handler(rules, validation_results, results) + for skipped_rule_id in skipped_rule_ids or []: + msg = ( + f"Rule '{skipped_rule_id}' was requested but is not available for " + f"standard {args.standard} version {args.version.replace('.', '-')}" + ) + engine_logger.info(msg) + results.append(RuleValidationResult.from_skipped_rule(skipped_rule_id, msg)) + # build all desired reports end = time.time() elapsed_time = end - start diff --git a/scripts/script_utils.py b/scripts/script_utils.py index ed73b2226..b9019f2ad 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -8,7 +8,7 @@ from cdisc_rules_engine.models.library_metadata_container import ( LibraryMetadataContainer, ) -from typing import List, Iterable +from typing import List, Iterable, Tuple from cdisc_rules_engine.config import config from cdisc_rules_engine.services import logger as engine_logger import os @@ -211,10 +211,15 @@ def get_cache_service(manager): return manager.InMemoryCacheService() -def get_rules(args) -> List[dict]: - return ( +def get_rules(args) -> Tuple[List[dict], List[str]]: + rules_result = ( load_rules_from_local(args) if args.local_rules else load_rules_from_cache(args) ) + if isinstance(rules_result, tuple) and len(rules_result) == 2: + rules, skipped_rule_ids = rules_result + else: + rules, skipped_rule_ids = rules_result, [] + return rules, skipped_rule_ids def rule_cache_file(args) -> str: @@ -268,6 +273,7 @@ def load_specified_rules( key = get_rules_cache_key(standard, version, substandard) standard_rules = standard_dict.get(key, {}) valid_rule_ids = set() + skipped_rule_ids = [] # Determine valid rules based on inclusion and exclusion lists for rule in standard_rules: @@ -276,7 +282,7 @@ def load_specified_rules( ): valid_rule_ids.add(rule) - # Log and skip any explicitly included rules that are not in the standard + # Log and collect any explicitly included rules that are not in the standard if rule_ids: for rule in rule_ids: if rule not in standard_rules: @@ -284,7 +290,10 @@ def load_specified_rules( f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}. " "It will be skipped from validation." ) - else: + skipped_rule_ids.append(rule) + + # Log and skip any explicitly excluded rules that are not in the standard + if excluded_rule_ids: for rule in excluded_rule_ids: if rule not in standard_rules: engine_logger.error( @@ -302,7 +311,7 @@ def load_specified_rules( engine_logger.error( f"All specified rules were excluded because they are not in the standard {standard} and version {version}" ) - return rules + return rules, skipped_rule_ids def load_all_rules_for_standard( @@ -337,7 +346,7 @@ def load_all_rules(rules_data): return rules -def load_rules_from_cache(args) -> List[dict]: +def load_rules_from_cache(args) -> list[dict] | tuple[list[dict], list[str]]: rules_file, cdisc_file, standard_dict = rule_cache_file(args) rules_data = {} try: @@ -356,6 +365,7 @@ def load_rules_from_cache(args) -> List[dict]: except Exception as e: engine_logger.error(f"Error loading rules file: {e}") return [] + if args.custom_standard: return load_custom_rules( rules_data, @@ -366,7 +376,7 @@ def load_rules_from_cache(args) -> List[dict]: standard_dict, ) elif args.rules or args.exclude_rules: - return load_specified_rules( + rules, skipped_rule_ids = load_specified_rules( rules_data, args.rules, args.exclude_rules, @@ -375,6 +385,7 @@ def load_rules_from_cache(args) -> List[dict]: standard_dict, args.substandard, ) + return rules, skipped_rule_ids elif args.standard and args.version: return load_all_rules_for_standard( rules_data, From 52006a27b0dd3ca4a1ea6b4b786064bf504f8904 Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Tue, 13 Jan 2026 13:11:55 +0100 Subject: [PATCH 3/5] Add regression tests for CORE-000354 rule validation and dataset structure --- .../test_Issues/test_CoreIssue1487.py | 54 ++++ tests/resources/CoreIssue1487/Datasets.json | 292 ++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 tests/QARegressionTests/test_Issues/test_CoreIssue1487.py create mode 100644 tests/resources/CoreIssue1487/Datasets.json diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py new file mode 100644 index 000000000..00c57b364 --- /dev/null +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py @@ -0,0 +1,54 @@ +import os +import subprocess +import unittest +import openpyxl +import pytest +from conftest import get_python_executable + + +@pytest.mark.regression +class TestCoreIssue1487(unittest.TestCase): + def test_positive_dataset(self): + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "5-0", + "-d", + os.path.join("tests", "resources", "CoreIssue1487"), + "-r", + "CORE-000354", + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + assert "Rules Report" in workbook.sheetnames + rules_sheet = workbook["Rules Report"] + target_row = None + for row in rules_sheet.iter_rows(min_row=2, values_only=True): + if row[0] == "CORE-000354": + target_row = row + break + assert target_row, "Rule CORE-000354 not present in 'Rules Report' sheet." + assert ( + target_row[4] and "was requested but is not available" in target_row[4] + ), "Expected error message for CORE-000354 not found." + assert target_row[5] == "SKIPPED", "CORE-000354 status should be SKIPPED." + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/resources/CoreIssue1487/Datasets.json b/tests/resources/CoreIssue1487/Datasets.json new file mode 100644 index 000000000..df7311972 --- /dev/null +++ b/tests/resources/CoreIssue1487/Datasets.json @@ -0,0 +1,292 @@ +{ + "datasets": [ + { + "filename": "ce.xpt", + "label": "Clinical Events", + "domain": "CE", + "variables": [ + { + "name": "STUDYID", + "label": "Study Identifier", + "type": "Char", + "length": 50 + }, + { + "name": "DOMAIN", + "label": "Domain Abbreviation", + "type": "Char", + "length": 50 + }, + { + "name": "USUBJID", + "label": "Unique Subject Identifier", + "type": "Char", + "length": 50 + }, + { + "name": "CESEQ", + "label": "Sequence Number", + "type": "Num", + "length": 8 + }, + { + "name": "CETERM", + "label": "Reported Term for the Clinical Event", + "type": "Char", + "length": 50 + }, + { + "name": "EPOCH", + "label": "Epoch", + "type": "Char", + "length": 50 + }, + { + "name": "CEDTC", + "label": "Start Date/Time of\r\nClinical Event", + "type": "Char", + "length": 50 + }, + { + "name": "CESTDTC", + "label": "Start Date/Time of Clinical Event", + "type": "Char", + "length": 50 + }, + { + "name": "CEENDTC", + "label": "End Date/Time of Clinical Event", + "type": "Char", + "length": 50 + }, + { + "name": "CEDY", + "label": "Study Day of Event\r\nCollection", + "type": "Num", + "length": 8 + }, + { + "name": "CESTDY", + "label": "Study Day of Start of Clinical Event", + "type": "Num", + "length": 8 + }, + { + "name": "CEENDY", + "label": "Study Day of End of Clinical Event", + "type": "Num", + "length": 8 + } + ], + "records": { + "STUDYID": ["CDISCPILOT01", "CDISCPILOT01"], + "DOMAIN": ["CE", "CE"], + "USUBJID": ["CDISC001", "CDISC008"], + "CESEQ": [1, 1], + "CETERM": ["COMPLETED SUICIDE", "CAR CRASH"], + "EPOCH": ["TREATMENT", "TREATMENT"], + "CEDTC": ["2014-10", "2014-10-31"], + "CESTDTC": ["2014-10-31", "2014-10-31"], + "CEENDTC": ["2014-10-31", "2014-10-31"], + "CEDY": [174, 174], + "CESTDY": [174, 174], + "CEENDY": [174, 174] + } + }, + { + "filename": "dm.xpt", + "label": "Demographics", + "domain": "DM", + "variables": [ + { + "name": "STUDYID", + "label": "Study Identifier", + "type": "Char", + "length": 50 + }, + { + "name": "DOMAIN", + "label": "Domain Abbreviation", + "type": "Char", + "length": 50 + }, + { + "name": "USUBJID", + "label": "Unique Subject Identifier", + "type": "Char", + "length": 50 + }, + { + "name": "SUBJID", + "label": "Subject Identifier for the Study", + "type": "Char", + "length": 50 + }, + { + "name": "RFSTDTC", + "label": "Subject Reference Start Date/Time", + "type": "Char", + "length": 50 + }, + { + "name": "RFENDTC", + "label": "Subject Reference End Date/Time", + "type": "Char", + "length": 50 + }, + { + "name": "RFXSTDTC", + "label": "Date/Time of First Study Treatment", + "type": "Char", + "length": 50 + }, + { + "name": "RFXENDTC", + "label": "Date/Time of Last Study Treatment", + "type": "Char", + "length": 50 + }, + { + "name": "RFICDTC", + "label": "Date/Time of Informed Consent", + "type": "Char", + "length": 50 + }, + { + "name": "RFPENDTC", + "label": "Date/Time of End of Participation", + "type": "Char", + "length": 50 + }, + { + "name": "DTHDTC", + "label": "Date/Time of Death", + "type": "Char", + "length": 50 + }, + { + "name": "DTHFL", + "label": "Subject Death Flag", + "type": "Char", + "length": 50 + }, + { + "name": "SITEID", + "label": "Study Site Identifier", + "type": "Char", + "length": 50 + }, + { + "name": "BRTHDTC", + "label": "Date/Time of Birth", + "type": "Char", + "length": 50 + }, + { + "name": "AGE", + "label": "Age", + "type": "Num", + "length": 8 + }, + { + "name": "AGEU", + "label": "Age Units", + "type": "Char", + "length": 50 + }, + { + "name": "SEX", + "label": "Sex", + "type": "Char", + "length": 50 + }, + { + "name": "RACE", + "label": "Race", + "type": "Char", + "length": 50 + }, + { + "name": "ETHNIC", + "label": "Ethnicity", + "type": "Char", + "length": 50 + }, + { + "name": "ARMCD", + "label": "Planned Arm Code", + "type": "Char", + "length": 50 + }, + { + "name": "ARM", + "label": "Description of Planned Arm", + "type": "Char", + "length": 50 + }, + { + "name": "ACTARMCD", + "label": "Actual Arm Code", + "type": "Char", + "length": 50 + }, + { + "name": "ACTARM", + "label": "Description of Actual Arm", + "type": "Char", + "length": 50 + }, + { + "name": "ARMNRS", + "label": "Reason Arm and/or Actual Arm is Null", + "type": "Char", + "length": 50 + }, + { + "name": "ACTARMUD", + "label": "Description of Unplanned Actual Arm", + "type": "Char", + "length": 50 + }, + { + "name": "COUNTRY", + "label": "Country", + "type": "Char", + "length": 50 + } + ], + "records": { + "STUDYID": ["CDISCPILOT01", "CDISCPILOT01"], + "DOMAIN": ["DM", "DM"], + "USUBJID": ["CDISC001", "CDISC008"], + "SUBJID": ["1115", "1116"], + "RFSTDTC": ["2012-11-30", "2012-11"], + "RFENDTC": ["2013-01-23", "2013-01-23"], + "RFXSTDTC": ["2012-11-30", "2012-11-30"], + "RFXENDTC": ["2013-01-23", "2013-01-23"], + "RFICDTC": ["2012-11-23", "2012-11-23"], + "RFPENDTC": ["2013-05-20", "2013-05-20"], + "DTHDTC": ["", ""], + "DTHFL": ["", ""], + "SITEID": ["701", "701"], + "BRTHDTC": ["1928", "1928"], + "AGE": [84, 84], + "AGEU": ["YEARS", "YEARS"], + "SEX": ["M", "M"], + "RACE": ["WHITE", "WHITE"], + "ETHNIC": ["NOT HISPANIC OR LATINO", "NOT HISPANIC OR LATINO"], + "ARMCD": ["ZAN_LOW", "ZAN_LOW"], + "ARM": ["Zanomaline Low Dose (54 mg)", "Zanomaline Low Dose (54 mg)"], + "ACTARMCD": ["ZAN_LOW", "ZAN_LOW"], + "ACTARM": [ + "Zanomaline Low Dose (54 mg)", + "Zanomaline Low Dose (54 mg)" + ], + "ARMNRS": ["", ""], + "ACTARMUD": ["", ""], + "COUNTRY": ["USA", "USA"] + } + } + ], + "codelists": [] +} From 982bb3ee409c0f47522cb74503be7f434660383d Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Tue, 13 Jan 2026 14:27:25 +0100 Subject: [PATCH 4/5] Refactor rule validation to improve handling of skipped rules and enhance logging --- scripts/run_validation.py | 10 ++++------ scripts/script_utils.py | 14 ++++++++++---- tests/unit/test_script_utils.py | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/scripts/run_validation.py b/scripts/run_validation.py index 8d7c71197..8ca1990e5 100644 --- a/scripts/run_validation.py +++ b/scripts/run_validation.py @@ -192,13 +192,11 @@ def run_validation(args: Validation_args): progress_handler: Callable = get_progress_displayer(args) results = progress_handler(rules, validation_results, results) - for skipped_rule_id in skipped_rule_ids or []: - msg = ( - f"Rule '{skipped_rule_id}' was requested but is not available for " - f"standard {args.standard} version {args.version.replace('.', '-')}" + for skipped_rule_id, message in skipped_rule_ids or []: + engine_logger.info(message) + results.append( + RuleValidationResult.from_skipped_rule(skipped_rule_id, message) ) - engine_logger.info(msg) - results.append(RuleValidationResult.from_skipped_rule(skipped_rule_id, msg)) # build all desired reports end = time.time() diff --git a/scripts/script_utils.py b/scripts/script_utils.py index b9019f2ad..bd6523517 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -211,7 +211,7 @@ def get_cache_service(manager): return manager.InMemoryCacheService() -def get_rules(args) -> Tuple[List[dict], List[str]]: +def get_rules(args) -> Tuple[List[dict], List[Tuple[str, str]]]: rules_result = ( load_rules_from_local(args) if args.local_rules else load_rules_from_cache(args) ) @@ -273,7 +273,7 @@ def load_specified_rules( key = get_rules_cache_key(standard, version, substandard) standard_rules = standard_dict.get(key, {}) valid_rule_ids = set() - skipped_rule_ids = [] + skipped_rule_ids: List[Tuple[str, str]] = [] # Determine valid rules based on inclusion and exclusion lists for rule in standard_rules: @@ -290,7 +290,11 @@ def load_specified_rules( f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}. " "It will be skipped from validation." ) - skipped_rule_ids.append(rule) + message = ( + f"Rule '{rule}' was requested but is not available for " + f"standard {standard} version {version}" + ) + skipped_rule_ids.append((rule, message)) # Log and skip any explicitly excluded rules that are not in the standard if excluded_rule_ids: @@ -346,7 +350,9 @@ def load_all_rules(rules_data): return rules -def load_rules_from_cache(args) -> list[dict] | tuple[list[dict], list[str]]: +def load_rules_from_cache( + args, +) -> list[dict] | tuple[list[dict], List[Tuple[str, str]]]: rules_file, cdisc_file, standard_dict = rule_cache_file(args) rules_data = {} try: diff --git a/tests/unit/test_script_utils.py b/tests/unit/test_script_utils.py index e878d9095..8122defdc 100644 --- a/tests/unit/test_script_utils.py +++ b/tests/unit/test_script_utils.py @@ -36,7 +36,7 @@ def test_load_specified_rules_include(standard_context): substandard, ) - returned_ids = {rule["core_id"] for rule in result} + returned_ids = {rule["core_id"] for rule in result[0] if isinstance(result, tuple)} assert returned_ids == {"CORE-0001", "CORE-0003"} @@ -59,7 +59,7 @@ def test_load_specified_rules_exclude(standard_context): substandard, ) - returned_ids = {rule["core_id"] for rule in result} + returned_ids = {rule["core_id"] for rule in result[0] if isinstance(result, tuple)} assert returned_ids == {"CORE-0001", "CORE-0003"} From e69e5a997ee2a638009dcd9e76e9400bef1ac9a3 Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Thu, 15 Jan 2026 03:26:42 +0100 Subject: [PATCH 5/5] Refactor rule loading logic --- scripts/script_utils.py | 111 ++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 39 deletions(-) diff --git a/scripts/script_utils.py b/scripts/script_utils.py index bd6523517..b08035d3a 100644 --- a/scripts/script_utils.py +++ b/scripts/script_utils.py @@ -261,6 +261,70 @@ def load_custom_rules(custom_data, cdisc_data, standard, version, rules, standar return list(rules_dict.values()) +def _determine_valid_rule_ids( + standard_rules: dict, + rule_ids: Iterable[str] | None, + excluded_rule_ids: Iterable[str] | None, +) -> set: + include_filter = set(rule_ids) if rule_ids else None + exclude_filter = set(excluded_rule_ids) if excluded_rule_ids else None + valid_rule_ids = set() + for rule in standard_rules: + if include_filter and rule not in include_filter: + continue + if exclude_filter and rule in exclude_filter: + continue + valid_rule_ids.add(rule) + return valid_rule_ids + + +def _collect_missing_includes( + rule_ids: Iterable[str] | None, + standard_rules: dict, + standard: str, + version: str, +) -> List[Tuple[str, str]]: + if not rule_ids: + return [] + available_rules = set(standard_rules) + skipped_rule_ids: List[Tuple[str, str]] = [] + for rule in rule_ids: + if rule in available_rules: + continue + engine_logger.error( + f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}. " + "It will be skipped from validation." + ) + message = ( + f"Rule '{rule}' was requested but is not available for " + f"standard {standard} version {version}" + ) + skipped_rule_ids.append((rule, message)) + return skipped_rule_ids + + +def _log_invalid_excludes( + excluded_rule_ids: Iterable[str] | None, + standard_rules: dict, + standard: str, + version: str, +) -> None: + if not excluded_rule_ids: + return + available_rules = set(standard_rules) + for rule in excluded_rule_ids: + if rule in available_rules: + continue + engine_logger.error( + f"The rule specified to exclude '{rule}' is not in the standard {standard} and version {version}. " + "It is not present and will be ignored." + ) + + +def _build_rules_from_ids(valid_rule_ids: set, rules_data) -> List[dict]: + return [rules_data.get(rule_id) for rule_id in valid_rule_ids] + + def load_specified_rules( rules_data, rule_ids, @@ -272,45 +336,14 @@ def load_specified_rules( ): key = get_rules_cache_key(standard, version, substandard) standard_rules = standard_dict.get(key, {}) - valid_rule_ids = set() - skipped_rule_ids: List[Tuple[str, str]] = [] - - # Determine valid rules based on inclusion and exclusion lists - for rule in standard_rules: - if (not rule_ids or rule in rule_ids) and ( - not excluded_rule_ids or rule not in excluded_rule_ids - ): - valid_rule_ids.add(rule) - - # Log and collect any explicitly included rules that are not in the standard - if rule_ids: - for rule in rule_ids: - if rule not in standard_rules: - engine_logger.error( - f"The rule specified to include '{rule}' is not in the standard {standard} and version {version}. " - "It will be skipped from validation." - ) - message = ( - f"Rule '{rule}' was requested but is not available for " - f"standard {standard} version {version}" - ) - skipped_rule_ids.append((rule, message)) - - # Log and skip any explicitly excluded rules that are not in the standard - if excluded_rule_ids: - for rule in excluded_rule_ids: - if rule not in standard_rules: - engine_logger.error( - f"The rule specified to exclude '{rule}' is not in the standard {standard} and version {version}. " - "It is not present and will be ignored." - ) - - rules = [] - for rule_id in valid_rule_ids: - rule_data = rules_data.get(rule_id) - rules.append(rule_data) - # If no valid rules were found, log an error but do not raise to avoid - # failing the whole run; the caller will receive an empty rules list. + valid_rule_ids = _determine_valid_rule_ids( + standard_rules, rule_ids, excluded_rule_ids + ) + skipped_rule_ids = _collect_missing_includes( + rule_ids, standard_rules, standard, version + ) + _log_invalid_excludes(excluded_rule_ids, standard_rules, standard, version) + rules = _build_rules_from_ids(valid_rule_ids, rules_data) if not rules: engine_logger.error( f"All specified rules were excluded because they are not in the standard {standard} and version {version}"