From c1fb1c6a0aef732c7b596b42d64b02c55f57b858 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Thu, 22 Jan 2026 13:35:09 -0500 Subject: [PATCH 1/9] initial commit tig use case --- TestRule/__init__.py | 2 + cdisc_rules_engine/constants/use_cases.py | 108 ------------------ cdisc_rules_engine/models/validation_args.py | 1 + cdisc_rules_engine/rules_engine.py | 2 + .../utilities/rule_processor.py | 29 ++--- core.py | 16 ++- scripts/run_validation.py | 2 + 7 files changed, 30 insertions(+), 130 deletions(-) delete mode 100644 cdisc_rules_engine/constants/use_cases.py diff --git a/TestRule/__init__.py b/TestRule/__init__.py index 3aa56cd28..0ccac2d0d 100644 --- a/TestRule/__init__.py +++ b/TestRule/__init__.py @@ -88,6 +88,7 @@ def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: # standard = standards_data.get("product") standard_version = standards_data.get("version") standard_substandard = standards_data.get("substandard") + use_case = standards_data.get("use_case") standard, standard_version = normalize_adam_input(standard, standard_version) codelists = json_data.get("codelists", []) cache = InMemoryCacheService() @@ -117,6 +118,7 @@ def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: # standard, standard_version, standard_substandard, + use_case, codelists, ) result = convert_numpy_types(result) diff --git a/cdisc_rules_engine/constants/use_cases.py b/cdisc_rules_engine/constants/use_cases.py deleted file mode 100644 index fdd8488c0..000000000 --- a/cdisc_rules_engine/constants/use_cases.py +++ /dev/null @@ -1,108 +0,0 @@ -""" -Constants for use cases and their allowed domains. -""" - -SDTM = "SDTM" -SEND = "SEND" -ADAM = "ADAM" -CDASH = "CDASH" - -INDH = "INDH" -PROD = "PROD" -NONCLIN = "NONCLIN" -ANALYSIS = "ANALYSIS" - -# NOTE: this may need to be expanded after the pilot re: custom domains, other applicable domains, etc. The -USE_CASE_DOMAINS = { - SDTM: { # only prod and individual health are allowed for sdtm - INDH: [ - "AE", - "CO", - "CM", - "DM", - "DI", - "DU", - "DO", - "DS", - "EG", - "EX", - "EC", - "FA", - "IE", - "LB", - "MH", - "PC", - "PP", - "DA", - "DV", - "QS", - "RELREC", - "RE", - "SC", - "SE", - "SV", - "SU", - "EM", - "TA", - "TE", - "TI", - "TS", - "TV", - "VS", - ], - PROD: ["TO", "PD", "PT", "IT", "IN", "IQ", "ES"], - NONCLIN: [], - ANALYSIS: [], - }, - SEND: { # only nonclin allowed for send - INDH: [], - PROD: [], - NONCLIN: [ - "BW", - "CV", - "CL", - "CO", - "DD", - "DM", - "DI", - "DU", - "DS", - "EG", - "EX", - "FW", - "GT", - "LB", - "MA", - "MI", - "OM", - "PM", - "PK", - "PP", - "POOLDEF", - "RELREC", - "RELREF", - "RE", - "SC", - "SE", - "TA", - "TE", - "TF", - "TX", - "TS", - "VS", - ], - ANALYSIS: [], - }, - ADAM: { # only analysis allowed for adam, ADAM AD-- prefix check is done elsewhere. This is here for completeness. - INDH: [], - PROD: [], - NONCLIN: [], - ANALYSIS: [], - }, - CDASH: { # no conformance rules for CDASH Presently - INDH: [], - PROD: [], - NONCLIN: [], - ANALYSIS: [], - }, -} diff --git a/cdisc_rules_engine/models/validation_args.py b/cdisc_rules_engine/models/validation_args.py index 961ac2bf4..03954bdba 100644 --- a/cdisc_rules_engine/models/validation_args.py +++ b/cdisc_rules_engine/models/validation_args.py @@ -11,6 +11,7 @@ "standard", "version", "substandard", + "use_case", "controlled_terminology_package", "output", "output_format", diff --git a/cdisc_rules_engine/rules_engine.py b/cdisc_rules_engine/rules_engine.py index e67c56b12..1c50f3493 100644 --- a/cdisc_rules_engine/rules_engine.py +++ b/cdisc_rules_engine/rules_engine.py @@ -70,6 +70,7 @@ def __init__( self.standard = kwargs.get("standard") self.standard_version = (kwargs.get("standard_version") or "").replace(".", "-") self.standard_substandard = kwargs.get("standard_substandard") or None + self.use_case = kwargs.get("use_case") or None self.library_metadata = kwargs.get("library_metadata") self.max_dataset_size = kwargs.get("max_dataset_size") self.dataset_paths = kwargs.get("dataset_paths") @@ -220,6 +221,7 @@ def validate_single_dataset( datasets, self.standard, self.standard_substandard, + self.use_case, ) if is_suitable: result: List[Union[dict, str]] = self.validate_rule( diff --git a/cdisc_rules_engine/utilities/rule_processor.py b/cdisc_rules_engine/utilities/rule_processor.py index c90ac7310..e6f86cb6b 100644 --- a/cdisc_rules_engine/utilities/rule_processor.py +++ b/cdisc_rules_engine/utilities/rule_processor.py @@ -24,7 +24,6 @@ SUPPLEMENTARY_DOMAINS, ) from cdisc_rules_engine.constants.rule_constants import ALL_KEYWORD -from cdisc_rules_engine.constants.use_cases import USE_CASE_DOMAINS from cdisc_rules_engine.interfaces import ConditionInterface from cdisc_rules_engine.models.operation_params import OperationParams from cdisc_rules_engine.models.rule_conditions import AllowedConditionsKeys @@ -274,6 +273,7 @@ def rule_applies_to_use_case( rule: dict, standard: str, standard_substandard: str, + use_case: str, ) -> bool: if standard.lower() != "tig": return True @@ -281,25 +281,7 @@ def rule_applies_to_use_case( if not use_cases: return True use_cases = [uc.strip() for uc in use_cases.split(",")] - substandard = standard_substandard.upper() - if substandard not in USE_CASE_DOMAINS: - return False - - domain_to_check = dataset_metadata.domain - if dataset_metadata.is_supp and dataset_metadata.rdomain: - domain_to_check = dataset_metadata.rdomain - - # Handle ADaM datasets with AD prefix - if substandard == "ADAM" and domain_to_check.startswith("AD"): - return "ANALYSIS" in use_cases - - allowed_domains = set() - for use_case in use_cases: - if use_case in USE_CASE_DOMAINS[substandard]: - allowed_domains.update(USE_CASE_DOMAINS[substandard][use_case]) - if domain_to_check in allowed_domains: - return True - return False + return use_case in use_cases @classmethod def rule_applies_to_entity( @@ -684,6 +666,7 @@ def is_suitable_for_validation( datasets: Iterable[SDTMDatasetMetadata], standard, standard_substandard: str, + use_case: str, ) -> Tuple[bool, str]: """Check if rule is suitable and return reason if not""" rule_id = rule.get("core_id", "unknown") @@ -698,7 +681,11 @@ def is_suitable_for_validation( ): return self.log_suitable_for_validation(rule_id, dataset_name) if not self.rule_applies_to_use_case( - dataset_metadata, rule, standard, standard_substandard + dataset_metadata, + rule, + standard, + standard_substandard, + use_case, ): reason = ( f"Rule skipped - doesn't apply to use case for " diff --git a/core.py b/core.py index 3d4d663fd..a46759566 100644 --- a/core.py +++ b/core.py @@ -228,7 +228,19 @@ def _validate_no_arguments(logger) -> None: "-ss", "--substandard", default=None, - help="CDISC Substandard to validate against", + type=click.Choice(["SDTM", "SEND", "ADaM", "CDASH"], case_sensitive=False), + help="CDISC Substandard to validate against. Any of SDTM, SEND, ADaM, CDASH", +) +@click.option( + "-uc", + "--use-case", + required=False, + default=None, + type=click.Choice(["INDH", "PROD", "NONCLIN", "ANALYSIS"], case_sensitive=True), + help=( + "CDISC TIG Use Case for scoping a TIG Validation." + "Any of INDH, PROD, NONCLIN, or ANALYSIS." + ), ) @click.option( "-ct", @@ -375,6 +387,7 @@ def validate( standard: str, version: str, substandard: str, + use_case: str, controlled_terminology_package: tuple[str], output: str, output_format: tuple[str], @@ -840,6 +853,7 @@ def test_validate(filetype): standard, version, None, + None, set(), output, output_format, diff --git a/scripts/run_validation.py b/scripts/run_validation.py index 8ca1990e5..e5b885f84 100644 --- a/scripts/run_validation.py +++ b/scripts/run_validation.py @@ -84,6 +84,7 @@ def validate_single_rule( standard=args.standard, standard_version=args.version.replace(".", "-"), standard_substandard=args.substandard, + use_case=args.use_case, external_dictionaries=args.external_dictionaries, ct_packages=args.controlled_terminology_package, define_xml_path=args.define_xml_path, @@ -234,6 +235,7 @@ def run_single_rule_validation( standard: str = None, standard_version: str = "", standard_substandard: str = None, + use_case: str = None, codelists=[], ) -> dict: datasets = [DummyDataset(dataset_data) for dataset_data in datasets] From eef8a1e69cd0e6b44a8088713ae788fe3f4e32ae Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Thu, 22 Jan 2026 13:42:48 -0500 Subject: [PATCH 2/9] validation arg --- core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core.py b/core.py index a46759566..e24e5097e 100644 --- a/core.py +++ b/core.py @@ -486,6 +486,7 @@ def validate( standard, version, substandard, + use_case, set(controlled_terminology_package), # avoiding duplicates output, set(output_format), # avoiding duplicates From 42a83e9c2e0f2e9884e3c0e90ff3fc41ccabb454 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Thu, 22 Jan 2026 14:02:24 -0500 Subject: [PATCH 3/9] use case test --- .../test_data_service/test_data_service.py | 1 + .../test_utilities/test_rule_processor.py | 52 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/tests/unit/test_services/test_data_service/test_data_service.py b/tests/unit/test_services/test_data_service/test_data_service.py index 4d7ded960..ab99220db 100644 --- a/tests/unit/test_services/test_data_service/test_data_service.py +++ b/tests/unit/test_services/test_data_service/test_data_service.py @@ -190,6 +190,7 @@ def test_get_dataset_class(dataset_metadata, data, expected_class): "sdtmig", "3-4", None, + None, "", "", "", diff --git a/tests/unit/test_utilities/test_rule_processor.py b/tests/unit/test_utilities/test_rule_processor.py index 4be952e66..f7272abfd 100644 --- a/tests/unit/test_utilities/test_rule_processor.py +++ b/tests/unit/test_utilities/test_rule_processor.py @@ -379,34 +379,35 @@ def test_rule_applies_to_class( @pytest.mark.parametrize( - "dataset_name, domain, rdomain, rule_use_case, standard, standard_substandard, outcome", + "dataset_name, domain, rdomain, rule_use_case, use_case, standard, standard_substandard, outcome", [ - # Basic use case tests with string format "INDH, PROD" - ("AE", "AE", None, "INDH, PROD", "tig", "SDTM", True), - ("CM", "CM", None, "INDH", "tig", "SDTM", True), - ("TS", "TS", None, "INDH", "tig", "SDTM", True), - ("ES", "ES", None, "PROD", "tig", "SDTM", True), - ("ES", "ES", None, "INDH", "tig", "SDTM", False), - ("BW", "BW", None, "NONCLIN", "tig", "SEND", True), - ("BW", "BW", None, "INDH", "tig", "SEND", False), + # Basic use case tests - user provides "INDH" or "PROD" + ("AE", "AE", None, "INDH, PROD", "INDH", "tig", "SDTM", True), + ("AE", "AE", None, "INDH, PROD", "PROD", "tig", "SDTM", True), + ("CM", "CM", None, "INDH", "INDH", "tig", "SDTM", True), + ("TS", "TS", None, "INDH", "INDH", "tig", "SDTM", True), + ("ES", "ES", None, "PROD", "PROD", "tig", "SDTM", True), + ("ES", "ES", None, "PROD", "INDH", "tig", "SDTM", False), + ("BW", "BW", None, "NONCLIN", "NONCLIN", "tig", "SEND", True), + ("BW", "BW", None, "NONCLIN", "INDH", "tig", "SEND", False), # Tests for ADaM datasets - ("ADSL", "ADSL", None, "ANALYSIS", "tig", "ADAM", True), - ("ADAE", "ADAE", None, "ANALYSIS", "tig", "ADAM", True), - ("ADAE", "ADAE", None, "INDH", "tig", "ADAM", False), + ("ADSL", "ADSL", None, "ANALYSIS", "ANALYSIS", "tig", "ADAM", True), + ("ADAE", "ADAE", None, "ANALYSIS", "ANALYSIS", "tig", "ADAM", True), + ("ADAE", "ADAE", None, "ANALYSIS", "INDH", "tig", "ADAM", False), # Tests for supplementary datasets - ("SUPPAE", None, "AE", "INDH", "tig", "SDTM", True), - ("SUPPQS", None, "QS", "INDH", "tig", "SDTM", True), - ("SUPPEC", None, "EC", "INDH", "tig", "SDTM", True), - ("SUPP--", None, "AE", "INDH", "tig", "SDTM", True), - ("SUPPPT", None, "PT", "PROD", "tig", "SDTM", True), - # Tests for empty/None use cases (should always return True) - ("AE", "AE", None, "", "tig", "SDTM", True), - ("AE", "AE", None, None, "tig", "SDTM", True), + ("SUPPAE", None, "AE", "INDH", "INDH", "tig", "SDTM", True), + ("SUPPQS", None, "QS", "INDH", "INDH", "tig", "SDTM", True), + ("SUPPEC", None, "EC", "INDH", "INDH", "tig", "SDTM", True), + ("SUPP--", None, "AE", "INDH", "INDH", "tig", "SDTM", True), + ("SUPPPT", None, "PT", "PROD", "PROD", "tig", "SDTM", True), + # Tests for empty/None use cases in rule (should always return True) + ("AE", "AE", None, "", "INDH", "tig", "SDTM", True), + ("AE", "AE", None, None, "INDH", "tig", "SDTM", True), # Tests for non-TIG standard (should always return True) - ("AE", "AE", None, "INDH", "sdtmig", "SDTM", True), - ("BW", "BW", None, "NONCLIN", "sendct", "SEND", True), - # Tests for substandards not in USE_CASE_DOMAINS - ("AE", "AE", None, "INDH", "tig", "UNKNOWN", False), + ("AE", "AE", None, "INDH", "INDH", "sdtmig", "SDTM", True), + ("BW", "BW", None, "NONCLIN", "NONCLIN", "sendct", "SEND", True), + # Test case mismatch + ("AE", "AE", None, "INDH, PROD", "SAFETY", "tig", "SDTM", False), ], ) def test_rule_applies_to_use_case( @@ -417,6 +418,7 @@ def test_rule_applies_to_use_case( rule_use_case, standard, standard_substandard, + use_case, outcome, ): processor = RuleProcessor(mock_data_service, InMemoryCacheService()) @@ -429,7 +431,7 @@ def test_rule_applies_to_use_case( ) assert ( processor.rule_applies_to_use_case( - dataset_metadata, rule, standard, standard_substandard + dataset_metadata, rule, standard, standard_substandard, use_case ) == outcome ) From 72e71659089591a04951dde00f403804c6e2322b Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Thu, 22 Jan 2026 14:05:52 -0500 Subject: [PATCH 4/9] last test failure --- tests/unit/test_services/test_data_service/test_data_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_services/test_data_service/test_data_service.py b/tests/unit/test_services/test_data_service/test_data_service.py index ab99220db..31ae87a91 100644 --- a/tests/unit/test_services/test_data_service/test_data_service.py +++ b/tests/unit/test_services/test_data_service/test_data_service.py @@ -275,6 +275,7 @@ def test_get_dataset_class_associated_domains(): "sdtmig", "3-4", None, + None, "", "", "", From 5fdc1a09c7d21592c735183c325f578e60131505 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Thu, 22 Jan 2026 14:16:56 -0500 Subject: [PATCH 5/9] readme --- README.md | 4 ++++ core.py | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 93f275e89..1aba04dad 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,10 @@ Run `python core.py validate --help` to see the list of validation options. -v, --version TEXT Standard version to validate against [required] -ss, --substandard TEXT Substandard to validate against + "SDTM", "SEND", "ADaM", or "CDASH" + [required for TIG] + -uc, --use-case TEXT Use Case for TIG Validation + "INDH", "PROD", "NONCLIN", or "ANALYSIS" [required for TIG] -ct, --controlled-terminology-package TEXT Controlled terminology package to validate diff --git a/core.py b/core.py index e24e5097e..d049469ea 100644 --- a/core.py +++ b/core.py @@ -375,7 +375,7 @@ def _validate_no_arguments(logger) -> None: ), ) @click.pass_context -def validate( +def validate( # noqa ctx, cache: str, pool_size: int, @@ -443,6 +443,12 @@ def validate( cache_path: str = os.path.join(os.path.dirname(__file__), cache) + if standard.lower() == "tig": + if not substandard or not use_case: + logger.error( + "Standard 'tig' requires both --substandard and --use-case to be specified." + ) + ctx.exit(2) # Construct ExternalDictionariesContainer: external_dictionaries = ExternalDictionariesContainer( { From 19f07079321027f495ab38f5a3ab7371575b1b42 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 23 Jan 2026 10:27:07 -0500 Subject: [PATCH 6/9] report tests, update --- .../services/reporting/sdtm_report_data.py | 26 +++++++++++------- resources/templates/report-template.xlsx | Bin 18217 -> 18258 bytes .../test_reporting/test_excel_export.py | 6 ++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cdisc_rules_engine/services/reporting/sdtm_report_data.py b/cdisc_rules_engine/services/reporting/sdtm_report_data.py index b9a258ab2..a6af212f8 100644 --- a/cdisc_rules_engine/services/reporting/sdtm_report_data.py +++ b/cdisc_rules_engine/services/reporting/sdtm_report_data.py @@ -67,12 +67,14 @@ def __init__( substandard = ( self._args.substandard if hasattr(self._args, "substandard") else None ) + use_case = self._args.use_case if hasattr(self._args, "use_case") else None self.data_sheets = { "Conformance Details": self.get_conformance_details_data( define_version, controlled_terminology, dictionary_versions, substandard=substandard, + use_case=use_case, ), "Dataset Details": self.get_dataset_details_data(), "Issue Summary": self.get_summary_data(), @@ -80,7 +82,7 @@ def __init__( "Rules Report": self.get_rules_report_data(), } - def get_conformance_details_data( + def get_conformance_details_data( # noqa self, define_version, cdiscCt, @@ -132,11 +134,15 @@ def get_conformance_details_data( conformance_details.append( ReportMetadataItem("Version", 11, f"V{self._version}") ) + if "use_case" in kwargs and kwargs["use_case"] is not None: + conformance_details.append( + ReportMetadataItem("TIG Use Case", 12, kwargs["use_case"]) + ) if cdiscCt: conformance_details.append( ReportMetadataItem( "CT Version", - 12, + 13, ( ", ".join(cdiscCt) if isinstance(cdiscCt, (list, tuple, set)) @@ -145,41 +151,41 @@ def get_conformance_details_data( ) ) else: - conformance_details.append(ReportMetadataItem("CT Version", 12, "")) + conformance_details.append(ReportMetadataItem("CT Version", 13, "")) conformance_details.append( - ReportMetadataItem("Define-XML Version", 13, define_version) + ReportMetadataItem("Define-XML Version", 14, define_version) ) # Populate external dictionary versions unii_version = dictionary_versions.get(DictionaryTypes.UNII.value) if unii_version is not None: conformance_details.append( - ReportMetadataItem("UNII Version", 16, unii_version) + ReportMetadataItem("UNII Version", 15, unii_version) ) medrt_version = dictionary_versions.get(DictionaryTypes.MEDRT.value) if medrt_version is not None: conformance_details.append( - ReportMetadataItem("Med-RT Version", 17, medrt_version) + ReportMetadataItem("Med-RT Version", 16, medrt_version) ) meddra_version = dictionary_versions.get(DictionaryTypes.MEDDRA.value) if meddra_version is not None: conformance_details.append( - ReportMetadataItem("MedDRA Version", 18, meddra_version) + ReportMetadataItem("MedDRA Version", 17, meddra_version) ) whodrug_version = dictionary_versions.get(DictionaryTypes.WHODRUG.value) if whodrug_version is not None: conformance_details.append( - ReportMetadataItem("WHODRUG Version", 19, whodrug_version) + ReportMetadataItem("WHODRUG Version", 18, whodrug_version) ) snomed_version = dictionary_versions.get(DictionaryTypes.SNOMED.value) if snomed_version is not None: conformance_details.append( - ReportMetadataItem("SNOMED Version", 20, snomed_version) + ReportMetadataItem("SNOMED Version", 19, snomed_version) ) loinc_version = dictionary_versions.get(DictionaryTypes.LOINC.value) if loinc_version is not None: conformance_details.append( - ReportMetadataItem("LOINC Version", 21, loinc_version) + ReportMetadataItem("LOINC Version", 20, loinc_version) ) return conformance_details diff --git a/resources/templates/report-template.xlsx b/resources/templates/report-template.xlsx index 2e805f055156cc9742847f3be01de12304c35c50..e2e47a62873245eeb8b9d826bcd618951ae94bde 100644 GIT binary patch delta 4585 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 delta 4500 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 diff --git a/tests/unit/test_services/test_reporting/test_excel_export.py b/tests/unit/test_services/test_reporting/test_excel_export.py index 5cd9aef6b..aa305b49a 100644 --- a/tests/unit/test_services/test_reporting/test_excel_export.py +++ b/tests/unit/test_services/test_reporting/test_excel_export.py @@ -30,6 +30,7 @@ def test_get_export(mock_validation_results): mock_args.standard = "sdtmig" mock_args.substandard = None mock_args.version = "3.4" + mock_args.use_case = None mock_args.dictionary_versions = {} datasets = [ SDTMDatasetMetadata( @@ -57,10 +58,11 @@ def test_get_export(mock_validation_results): assert wb["Conformance Details"]["B9"].value == "SDTMIG" assert wb["Conformance Details"]["B10"].value == "NAP" assert wb["Conformance Details"]["B11"].value == "V3.4" - assert wb["Conformance Details"]["B12"].value == ", ".join( + assert wb["Conformance Details"]["B12"].value == "NAP" + assert wb["Conformance Details"]["B13"].value == ", ".join( mock_args.controlled_terminology_package ) - assert wb["Conformance Details"]["B13"].value == "2.1" + assert wb["Conformance Details"]["B14"].value == "2.1" # Check dataset details tab assert wb["Dataset Details"]["A2"].value == "test" # filename From e95273e439a39bfe176737d936800f8b4ec0eeeb Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 23 Jan 2026 12:46:00 -0500 Subject: [PATCH 7/9] use case ruletester --- scripts/run_validation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/run_validation.py b/scripts/run_validation.py index e5b885f84..3f6df90bb 100644 --- a/scripts/run_validation.py +++ b/scripts/run_validation.py @@ -292,6 +292,7 @@ def run_single_rule_validation( standard=standard, standard_version=standard_version, standard_substandard=standard_substandard, + use_case=use_case, library_metadata=library_metadata, ) engine.rule_processor = RuleProcessor(data_service, cache, library_metadata) From fffcd8ca36d7dbeedacb7b1b752276702470176e Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 23 Jan 2026 13:21:57 -0500 Subject: [PATCH 8/9] fix parsing issue --- cdisc_rules_engine/utilities/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdisc_rules_engine/utilities/utils.py b/cdisc_rules_engine/utilities/utils.py index f3b8906a5..2b4a47163 100644 --- a/cdisc_rules_engine/utilities/utils.py +++ b/cdisc_rules_engine/utilities/utils.py @@ -178,7 +178,7 @@ def get_standard_details_cache_key( if not standard_substandard: return f"standards/{standard_type}/{standard_version}" else: - return f"standards/{standard_type}/{standard_version}/{standard_substandard}" + return f"standards/{standard_type}/{standard_version}/{standard_substandard.lower()}" def normalize_adam_input(standard: str, version: str) -> tuple: From c6e7d702ade6c45838369d7866cef46aa106fb73 Mon Sep 17 00:00:00 2001 From: Samuel Johnson Date: Fri, 23 Jan 2026 13:36:54 -0500 Subject: [PATCH 9/9] update --- core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core.py b/core.py index d049469ea..51508cd02 100644 --- a/core.py +++ b/core.py @@ -228,7 +228,7 @@ def _validate_no_arguments(logger) -> None: "-ss", "--substandard", default=None, - type=click.Choice(["SDTM", "SEND", "ADaM", "CDASH"], case_sensitive=False), + type=click.Choice(["sdtm", "send", "adam", "cdash"], case_sensitive=False), help="CDISC Substandard to validate against. Any of SDTM, SEND, ADaM, CDASH", ) @click.option(