How coreval works

This is the reference for people who need to know what coreval does with their data before they rely on it: which files it reads, which rules it runs and why, what it does when a rule cannot be answered, and what every part of a result means. For a quick start, see vignette("coreval") instead.

What coreval is

coreval runs the CDISC Open Rules (CORE) conformance rules inside R. It does not call CDISC’s engine. It reads each rule’s published definition and evaluates it with its own implementation of the rule language.

Rules are data, operators are code. Every rule ships as data, extracted from the upstream repositories at a pinned commit. The R code implements the operators the rules are written in (equal_to, is_complete_date, is_unique_set, and so on). No rule has a function of its own, so a new rule upstream needs no new code unless it uses a new operator.

At run time it needs only R, data.table and haven. There is no Python, no network access and no account. Everything it uses, from the rules and the standards metadata to every Controlled Terminology package CDISC’s engine carries, from 2014 on, is bundled.

The two rule sets

In November 2026 CORE 1.0 split the rules between two repositories, and coreval bundles both:

Rule set Repository Used for
CORE 1.0 verisianHQ/core-contributor SDTMIG 3.2, 3.3 and 3.4, and the FDA Business Rules
CDISC cdisc-org/cdisc-open-rules SENDIG, TIG and USDM

Which set runs follows from the standard:

Some rules exist in both repositories under the same id. A study checked as SDTM gets the CORE 1.0 version. A study checked as SEND or TIG gets CDISC’s version, narrowed to those standards.

The two engines behind these rule sets do not behave identically, and coreval follows each set’s own engine. The difference you are most likely to meet is a rule that reads a column the dataset does not have:

docs/REFERENCE-BEHAVIOUR.md in the repository lists every difference coreval knows of, with the engine source that settles each one.

Which version you are running

attr(list_rules(), "rules_version")
#> "cdisc-org/cdisc-open-rules af05dfd2...; verisianHQ/core-contributor a3b4d0b0..."

Both commits are recorded with every file write_findings() saves, so a saved report always says which rules produced it.

Status of a rule

list_rules() returns one row per bundled rule. Its source column says where the rule came from and how settled it is:

source Meaning Runs by default
core_contributor CORE 1.0’s published SDTM and FDA rules yes
published CDISC’s published rules yes
sendig_draft drafts in CDISC’s repository yes
sdtmig_draft, fda_business_rules_draft drafts in CDISC’s repository for SEND and TIG; not for SDTM, see below
deprecated_dir retired by CDISC no

Retired rules, and CDISC’s SDTM rules that CORE 1.0 no longer carries, run only with include_deprecated = TRUE. That second group takes in the SDTMIG drafts, the FDA drafts written for SDTMIG and two published rules, since CORE 1.0 replaced them; the same rules still run for SEND or TIG where CDISC wrote them for those too. They are off by default because a retired rule usually has a current replacement, and running both reports the same defect twice.

list_rules() itself includes the retired ones, because it lists what is bundled, not what will run.

Reading data

A study folder

check_study(path, standard = ) and read_study(path) read every dataset in the folder:

Format Extension Notes
SAS transport .xpt A file holding more than one dataset is refused.
SAS dataset .sas7bdat
Dataset-JSON .json, .ndjson Needs the jsonlite package. The name the file declares is used over the file name.
Define-XML any .xml named like a define Needs the xml2 package.
USDM .json Only with usdm = TRUE, in a folder of its own.

Some details that matter in practice:

One dataset

check_dataset(x, standard = ) takes a data frame, or one .xpt, .sas7bdat or .csv file. The domain comes from the DOMAIN column; only a dataset with no DOMAIN column falls back to the file name. Set domain = to override either.

A bare CSV has no declared types, so its types are inferred, and a numeric-looking identifier will be read as a number. Prefer .xpt when that matters. A CSV row with too many or too few fields is refused rather than read partly.

A data frame built in R has no variable labels. The few rules that compare labels are skipped on one, with a reason saying so.

Which rules run

For every dataset, coreval selects the rules that apply to it and then evaluates each one. Selection uses, in order:

  1. The rule set, chosen from the standard as described above.
  2. Status. Retired rules are left out unless include_deprecated = TRUE.
  3. Standard and version. With standard (and optionally version) set, only rules written for that standard and version run. The number set aside is kept in the excluded_by_standard attribute and shown in the report. A check without a standard is refused, as CDISC’s and Verisian’s engines refuse it: running every standard’s rules put SEND and TIG problems in an SDTM report. A few rules CDISC wrote for both SDTM and SEND or TIG run only when standard names SEND or TIG, since CORE 1.0 replaced their SDTM half.
  4. Domain and class. A rule’s scope names the domains and observation classes it applies to. A domain’s class (Events, Findings, …) is looked up from the bundled SDTMIG metadata. For a CORE 1.0 rule, the declared SDTMIG version also decides whether the IG defines the domain at all.
  5. Use case, if you pass use_case (for example "INDH"). A TIG check needs substandard too, "SDTM" or "SEND" (or "ADaM"): a TIG rule’s use case covers different datasets in each, and CDISC’s engine will not check a TIG study without it.

A study folder does not declare its standard, so for a folder of datasets standard has to be passed. A CORE test case or a USDM file declares its own, and an explicit standard overrides it.

Controlled terminology

Rules that ask whether a value is an allowed term need to know which Controlled Terminology release the study follows, because terms are added and removed between releases. coreval takes it from, in order:

  1. ct_package, if you pass one (for example "sdtmct-2026-03-27");
  2. the study’s TS dataset: the TSVCDVER column, on rows whose TSVCDREF is CDISC or CDISC CT;
  3. the define.xml’s def:Standards declaration.

If none of those names a release, the terminology rules are skipped with a reason. coreval never picks a release for you: judged against a release the study never declared, the data would show violations that are not there and hide some that are. list_ct_packages() lists every bundled release.

Dictionaries

MedDRA, WHODrug and the other licensed dictionaries are not bundled and cannot be read. Rules that need one are skipped with a reason naming the dictionary.

Evaluation

Every dataset in a study is read once, then every applicable rule is run against it. check_study() and check_dataset() go through the same evaluation loop, so a rule behaves the same in both. They differ in what counts as missing: checking one dataset, every other dataset is unsupplied, and rules that need one are skipped, as are the rules about the study as a whole. Checking a study, a rule that needs a dataset the study does not have is skipped too, saying the dataset is not in the study, except where the rule itself says what an absent one means: an optional (left) join, and a CORE 1.0 rule computing a set of values from it, which is then empty.

A rule can be about:

Rules that join datasets (a supplemental qualifier to its parent, a record to DM, RELREC relationships) do the join first and evaluate the joined rows.

When a rule cannot be answered

Some rules cannot be evaluated on a given dataset. coreval never reports one of those as passing. Each goes into result$skipped with a reason. The common ones:

Reason What it means What to do
needs DM, which was not supplied the rule compares with another dataset, and you checked one on its own run check_study() on the folder
needs TV, which is not in this study the dataset it compares with does not exist in the study nothing, if the study has no TV
asks which datasets the whole study contains, asks about the whole study a rule about the study as a whole, asked of one dataset run check_study()
needs controlled terminology no CT release was found pass ct_package, or fill TSVCDREF and TSVCDVER in TS
… needs define.xml … the rule compares data with the define add the define to the folder and install xml2
needs AESER, which this dataset does not have a CDISC rule reads a column the dataset lacks often nothing: the column may be legitimately absent
needs the MedDRA dictionary dictionary not bundled check with your licensed tool
an operation groups by EPOCH, which the dataset does not have a computed step needs a missing column as above
unimplemented Operations type coreval cannot evaluate this rule at all check with your qualified tool

Many of these are expected and harmless. Read them anyway, because an empty findings table means either clean data or rules that never ran, and the skipped table is the only way to tell which.

A check that cannot run at all, because nothing applies or the input is unusable, stops with an error rather than printing a clean report. If every check that applied had to be skipped, the report says that no check ran.

The result

check_dataset() and check_study() both return an object of class coreval_result. It is a list of three data tables:

findings

One row per affected (dataset, record, variable).

Column Meaning
Dataset dataset name, or STUDY for study-level rules
Record row number in the dataset, counting from 1; blank for dataset-level rules
USUBJID the record’s subject, blank where the dataset has none or the row is not a record
SEQ the record’s --SEQ, blank in the same way
Variable the variable the rule reports
Value the value it found; Not in dataset when the variable does not exist
issue the rule’s message, in words
triage wrong value, missing required or missing optional
rule_id the CORE rule id

A Variable beginning with $ is a value the rule computed rather than a column of your data, for example $dm_studyid, the set of STUDYID values found in DM. It is shown so you can see what the record was compared against.

skipped

One row per rule and dataset that could not be checked: rule_id, domain and reason.

truncated

Rules that flagged more records than max_records (1,000 by default) kept: rule_id, domain, records_found and records_kept. A rule flagging every row of a large dataset would otherwise produce more rows than anyone can read or Excel can hold. The true count is always kept here, and the report says “or more” where counts are capped. Use max_records = Inf to keep everything.

Attributes

Attribute Meaning
checks_run how many rule-and-dataset checks were evaluated
domains the datasets checked
excluded_by_standard rules set aside by standard / version
define_only TRUE when the folder held a define.xml and no datasets

Triage

CDISC’s rules carry no severity, so coreval does not invent one. It sorts each problem (one rule in one dataset) into one of three groups instead:

The printed report orders problems by triage group, then by how many records they touch. Within a problem, records holding a real offending value are shown before blank ones.

Printing, summarising, saving

Speed and memory

The whole study is held in memory, as data tables, while it is checked. Memory grows roughly in step with the number of rows. docs/BENCHMARKS.md in the repository has measured times and peak memory on CDISC’s pilot submission and on copies of it up to 5.9 million rows, with the scripts to reproduce them.

Some practical points:

Optional packages

Package Needed for
xml2 reading define.xml and the study tagging file
jsonlite Dataset-JSON and USDM study files
QuickJSR USDM’s JSONata rules
writexl saving to Excel

Without one, the rules or output that need it are skipped or refused with a message naming the package. Nothing else changes.

How the results are checked

For most rules the upstream repositories publish worked examples: small datasets, and the exact records a correct implementation should flag. coreval’s conformance harness runs every rule against every example and compares the result row by row. More than nine in ten rules in each set match exactly. docs/COVERAGE.md goes through every rule that does not, and why.

docs/REAL-STUDY.md compares coreval with CORE 1.0’s own engine on CDISC’s pilot submission, finding by finding, and explains every difference.

Those examples are small and tidy, and real submissions are neither, so the agreement rate is a floor on correctness rather than a score.

coreval and the CORE engines

Three programs run these rules:

The engines are the official reference: when coreval and an engine disagree and nothing else settles it, the engine is right. They also check terms against the licensed dictionaries (MedDRA, WHODrug and others) when you supply them, which coreval cannot do.

coreval is built for a different moment. It runs inside R, on a data frame you have open as well as on a folder of files. It needs no Python, database, network or API key, and it reports every rule it could not run, with the reason, next to the findings.

A sensible split is coreval while you build the datasets, the engines before submission, and your organisation’s qualified tool for the submission itself.

docs/COMPARISON.md in the repository compares the three in detail, with measured speed and memory and every difference found on CDISC’s pilot study. It lives in the repository rather than here because those details change with each release of the engines.

What it does not do

coreval is not validated software and is not a certified CORE engine. Treat its findings as a fast first check, and run your qualified tool before submission.