From 9e209e54041338403e3b40a45eda76f4a15a5c4f Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Feb 2026 14:33:08 -0500 Subject: [PATCH 1/4] rule type, builder changes --- .../domain_list_with_define_builder.py | 6 +++--- .../enums/domain_presence_values.py | 1 + cdisc_rules_engine/enums/sensitivity.py | 1 + cdisc_rules_engine/models/actions.py | 6 ++++++ cdisc_rules_engine/rules_engine.py | 18 +++++++++++++++++- resources/schema/rule/Rule_Type.md | 4 ++-- resources/schema/rule/Sensitivity.json | 3 +++ resources/schema/rule/Sensitivity.md | 4 ++++ resources/templates/report-template.xlsx | Bin 20694 -> 20500 bytes 9 files changed, 37 insertions(+), 6 deletions(-) diff --git a/cdisc_rules_engine/dataset_builders/domain_list_with_define_builder.py b/cdisc_rules_engine/dataset_builders/domain_list_with_define_builder.py index 180603763..8ec2741c3 100644 --- a/cdisc_rules_engine/dataset_builders/domain_list_with_define_builder.py +++ b/cdisc_rules_engine/dataset_builders/domain_list_with_define_builder.py @@ -31,11 +31,11 @@ def build(self): records = [] for define_item in all_define_metadata: domain_name = define_item.get("define_dataset_name", "") + filename = domain_files.get(domain_name) record = { - "domain": domain_name, - "filename": domain_files.get(domain_name), **define_item, + "domain": domain_name if filename is not None else None, + "filename": filename, } records.append(record) - return self.dataset_implementation.from_records(records) diff --git a/cdisc_rules_engine/enums/domain_presence_values.py b/cdisc_rules_engine/enums/domain_presence_values.py index bdd5b687c..5e664c05f 100644 --- a/cdisc_rules_engine/enums/domain_presence_values.py +++ b/cdisc_rules_engine/enums/domain_presence_values.py @@ -4,3 +4,4 @@ class DomainPresenceValues(BaseEnum): DATASET = "STUDY" RECORD = "" + DOMAIN = "N/A" diff --git a/cdisc_rules_engine/enums/sensitivity.py b/cdisc_rules_engine/enums/sensitivity.py index 4d2189129..7ff7ea376 100644 --- a/cdisc_rules_engine/enums/sensitivity.py +++ b/cdisc_rules_engine/enums/sensitivity.py @@ -5,3 +5,4 @@ class Sensitivity(BaseEnum): DATASET = "Dataset" RECORD = "Record" GROUP = "Group" + STUDY = "Study" diff --git a/cdisc_rules_engine/models/actions.py b/cdisc_rules_engine/models/actions.py index 331a62ed7..60f493af9 100644 --- a/cdisc_rules_engine/models/actions.py +++ b/cdisc_rules_engine/models/actions.py @@ -65,8 +65,10 @@ def generate_dataset_error_objects(self, message: str, results: pd.Series): ) if "domain presence" in self.rule.get("rule_type", "").lower(): error_object.dataset = DomainPresenceValues.DATASET.value + error_object.domain = DomainPresenceValues.DOMAIN.value for error in error_object.errors: error.dataset = DomainPresenceValues.DATASET.value + error.domain = DomainPresenceValues.DATASET.value error.row = DomainPresenceValues.RECORD.value self.output_container.append(error_object.to_representation()) @@ -177,6 +179,10 @@ def generate_targeted_error_object( # noqa: C901 errors_list = self._generate_errors_by_target_presence( data, targets_not_in_dataset, all_targets_missing, errors_df ) + elif self.rule.get("sensitivity") == Sensitivity.STUDY.value: + errors_list = self._generate_errors_by_target_presence( + data, targets_not_in_dataset, all_targets_missing, errors_df + ) elif self.rule.get("sensitivity") == Sensitivity.GROUP.value: grouping_variables = self.rule.get("grouping_variables", []) diff --git a/cdisc_rules_engine/rules_engine.py b/cdisc_rules_engine/rules_engine.py index 05666741d..50f36b009 100644 --- a/cdisc_rules_engine/rules_engine.py +++ b/cdisc_rules_engine/rules_engine.py @@ -1,6 +1,8 @@ from copy import deepcopy from typing import Iterable, List, Union from dateutil.parser._parser import ParserError +import traceback + from business_rules import export_rule_data from business_rules.engine import run from cdisc_rules_engine.config import config as default_config @@ -58,7 +60,7 @@ ExternalDictionariesContainer, ) from cdisc_rules_engine.models.sdtm_dataset_metadata import SDTMDatasetMetadata -import traceback +from cdisc_rules_engine.enums.sensitivity import Sensitivity class RulesEngine: @@ -174,6 +176,8 @@ def validate_single_rule(self, rule: dict, datasets: Iterable[SDTMDatasetMetadat ) if limit_reached: break + if rule.get("sensitivity") == Sensitivity.STUDY.value: + results = self._collapse_to_study_result(results) return results def _update_total_errors_and_check_limit( @@ -671,3 +675,15 @@ def handle_validation_exceptions( # noqa message=message, status=ExecutionStatus.EXECUTION_ERROR.value, ) + + def _collapse_to_study_result(self, results: dict) -> dict: + """ + For study sensitivity rules, collapse all per-dataset results into a single + study-level result using the first non-skipped result as the representative. + """ + for key, dataset_results in results.items(): + for result in dataset_results: + if result.get("executionStatus") != ExecutionStatus.SKIPPED.value: + return {"study": [result]} + first_key = next(iter(results)) + return {"study": results[first_key]} diff --git a/resources/schema/rule/Rule_Type.md b/resources/schema/rule/Rule_Type.md index 9556730f6..6d534755b 100644 --- a/resources/schema/rule/Rule_Type.md +++ b/resources/schema/rule/Rule_Type.md @@ -192,8 +192,8 @@ all: One row per dataset defined in Define-XML: -- `domain` -- `filename` - The file name if dataset exists, null otherwise +- `domain` - The domain if the dataset exists, empty otherwise +- `filename` - The file name if dataset exists, empty otherwise - `define_dataset_name` - `define_dataset_label` - `define_dataset_location` diff --git a/resources/schema/rule/Sensitivity.json b/resources/schema/rule/Sensitivity.json index 08c68d6bd..ba331d1a6 100644 --- a/resources/schema/rule/Sensitivity.json +++ b/resources/schema/rule/Sensitivity.json @@ -10,6 +10,9 @@ }, { "const": "Group" + }, + { + "const": "Study" } ], "markdownDescription": "Determines what level of granularity issues should be generated within the report" diff --git a/resources/schema/rule/Sensitivity.md b/resources/schema/rule/Sensitivity.md index 3229a521f..45ffb9d92 100644 --- a/resources/schema/rule/Sensitivity.md +++ b/resources/schema/rule/Sensitivity.md @@ -1,3 +1,7 @@ +## Study + +Will report once per submitted data study. Lends itself to Domain Presence Check rule types as these are cross-study checks that do not involve within-dataset checks. Reports one result per failing row in the dataset generated by the rule type, collapsed to a single study-level result. + ## Dataset Report one result per dataset generated by the `Rule Type`, where a dataset or record within the dataset matches the rule failure criteria diff --git a/resources/templates/report-template.xlsx b/resources/templates/report-template.xlsx index 41b3eb81a5674513832c80179777143cbbc205a0..227cdd1e74e22f6873af4f354a25e1534275a30e 100644 GIT binary patch delta 11841 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 delta 12066 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 SAe6Io{JtUuoWI;-+y4RcM!k;! From 25aa36ead3915449c7851f6c7973ee7dda39bc1d Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Feb 2026 15:07:28 -0500 Subject: [PATCH 2/4] tests --- .../test_domain_presence_define_builder.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_dataset_builders/test_domain_presence_define_builder.py b/tests/unit/test_dataset_builders/test_domain_presence_define_builder.py index 70ccb61c3..3005e31cc 100644 --- a/tests/unit/test_dataset_builders/test_domain_presence_define_builder.py +++ b/tests/unit/test_dataset_builders/test_domain_presence_define_builder.py @@ -199,7 +199,7 @@ ], }, { - "domain": "SE", + "domain": None, "filename": None, "define_dataset_name": "SE", "define_dataset_label": "Subject Elements", @@ -241,7 +241,7 @@ pd.DataFrame( [ { - "domain": "AE", + "domain": None, "filename": None, "define_dataset_name": "AE", "define_dataset_label": "Adverse Events", @@ -260,7 +260,7 @@ ], }, { - "domain": "DM", + "domain": None, "filename": None, "define_dataset_name": "DM", "define_dataset_label": "Demographics", @@ -279,7 +279,7 @@ ], }, { - "domain": "SE", + "domain": None, "filename": None, "define_dataset_name": "SE", "define_dataset_label": "Subject Elements", @@ -293,7 +293,7 @@ "define_dataset_variables": ["STUDYID", "USUBJID", "SESEQ"], }, { - "domain": "EC", + "domain": None, "filename": None, "define_dataset_name": "EC", "define_dataset_label": "Exposure as Collected", @@ -344,7 +344,11 @@ def test_domain_list_with_define_dataset_builder( if expected_results.empty: assert result_df.empty, f"Expected empty DataFrame for {test_description}" else: - assert list(result_df.columns) == list( + assert set(result_df.columns) == set( expected_results.columns ), f"Columns do not match for {test_description}" - pd.testing.assert_frame_equal(result_df, expected_results, check_dtype=False) + pd.testing.assert_frame_equal( + result_df.reindex(columns=expected_results.columns), + expected_results, + check_dtype=False, + ) From 3c5df3f69f7a3db3e859abf7caaf22b4ad1144a9 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 13 Feb 2026 15:41:46 -0500 Subject: [PATCH 3/4] last test --- tests/unit/test_rules_engine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_rules_engine.py b/tests/unit/test_rules_engine.py index a068ac9a7..62a4bd040 100644 --- a/tests/unit/test_rules_engine.py +++ b/tests/unit/test_rules_engine.py @@ -1050,7 +1050,7 @@ def test_rule_with_domain_prefix_replacement(mock_get_dataset: MagicMock): { "executionStatus": ExecutionStatus.ISSUE_REPORTED.value, "dataset": "STUDY", - "domain": "AE", + "domain": "N/A", "variables": ["AE"], "message": "Domain AE exists", "errors": [ From d79c3de14aeb6515cbc45bdab96d1c38518ab9aa Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Mon, 16 Feb 2026 09:21:32 -0500 Subject: [PATCH 4/4] docs --- cdisc_rules_engine/models/actions.py | 1 - resources/schema/rule/Rule_Type.md | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cdisc_rules_engine/models/actions.py b/cdisc_rules_engine/models/actions.py index 60f493af9..2150d94c6 100644 --- a/cdisc_rules_engine/models/actions.py +++ b/cdisc_rules_engine/models/actions.py @@ -68,7 +68,6 @@ def generate_dataset_error_objects(self, message: str, results: pd.Series): error_object.domain = DomainPresenceValues.DOMAIN.value for error in error_object.errors: error.dataset = DomainPresenceValues.DATASET.value - error.domain = DomainPresenceValues.DATASET.value error.row = DomainPresenceValues.RECORD.value self.output_container.append(error_object.to_representation()) diff --git a/resources/schema/rule/Rule_Type.md b/resources/schema/rule/Rule_Type.md index 6d534755b..bbf184d8f 100644 --- a/resources/schema/rule/Rule_Type.md +++ b/resources/schema/rule/Rule_Type.md @@ -192,8 +192,8 @@ all: One row per dataset defined in Define-XML: -- `domain` - The domain if the dataset exists, empty otherwise -- `filename` - The file name if dataset exists, empty otherwise +- `domain` - The domain if the dataset exists, null otherwise +- `filename` - The file name if dataset exists, null otherwise - `define_dataset_name` - `define_dataset_label` - `define_dataset_location`