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.
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.
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:
standard = "SENDIG" (or any SEND variant) or
"TIG" uses CDISC’s set."SDTMIG" uses the CORE 1.0 set. "ADaMIG"
and "USDM" use CDISC’s rules for those standards.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.
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.
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.
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:
ae.xpt is AE, suppae.xpt is
SUPPAE. Split datasets (lb1.xpt,
lb2.xpt) keep their own names and are recognised as parts
of LB.dm.xpt and dm.sas7bdat during a conversion:
the transport file wins, then the SAS dataset, then Dataset-JSON, and a
warning names the file left out. Two files of the same format for one
dataset are refused, since there is no telling which is meant._variables.csv, is the exception),
because a CSV carries no column types or labels. They are named in a
warning so they are not missed. Check a CSV on its own with
check_dataset().define.xml is preferred. Failing that, any XML file whose
name contains “define” is used, with a warning if there is more than one
candidate, and a folder holding a single XML file uses that one whatever
its name. The define gives rules the variable metadata, value-level
metadata and controlled terminology version they compare against."" for a missing value, as SAS transport files do, and
numeric columns hold NA. Column types are taken from the
file, never guessed, so an identifier like "007" stays
text.check_study() refuses, with an error, an empty folder,
a folder that does not exist, or a folder holding nothing coreval can
read. It never reports one as a clean study. read_study()
refuses a missing folder, and returns an empty study for an empty
one.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.
For every dataset, coreval selects the rules that apply to it and then evaluates each one. Selection uses, in order:
include_deprecated = TRUE.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.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.
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:
ct_package, if you pass one (for example
"sdtmct-2026-03-27");TS dataset: the TSVCDVER
column, on rows whose TSVCDREF is CDISC or
CDISC CT;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.
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.
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:
Record set to the row number;Record is blank;Dataset = "STUDY".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.
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.
check_dataset() and check_study() both
return an object of class coreval_result. It is a list of
three data tables:
findingsOne 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.
skippedOne row per rule and dataset that could not be checked:
rule_id, domain and reason.
truncatedRules 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.
| 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 |
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:
wrong value: at least one reported
variable holds a value, and that value breaks the rule. The data itself
is the evidence. Identifiers such as USUBJID and
--SEQ do not count as evidence, since they only say which
record is meant.missing required: nothing is present,
and the rule concerns something the standard marks Required, or a value
is blank in a variable that exists.missing optional: nothing is present,
and it is Expected or Permissible, or the variable is simply not in the
dataset. Often legitimate.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.
print(result, n = 10, rows = 3, guidance = FALSE)
prints the report. n is the number of problems to describe
(per dataset for a study), rows the example records per
problem, and guidance = TRUE adds the Implementation Guide
sentence each rule enforces.summary(result) returns a one-row table: problems,
records, counts per triage group, checks run, checks that could not run,
and capped rules. It binds across results with
rbind().filter_findings(result, triage =, dataset =, rule =, variable =)
returns a narrowed result that still prints, summarises and saves. A
saved file records that it was filtered.write_findings(result, "issues.xlsx") writes one
workbook with sheets Problems, Records,
Not checked and Rules. A .csv
path writes one file per table (issues.csv,
issues_summary.csv, issues_skipped.csv,
issues_about.csv). The foot of Not checked,
like issues_about.csv, records the coreval version, the
rules commit, the standard, version and terminology used, the number of
checks run, and whether the file is the whole result. In CSV output a
text value starting with =, +, -
or @ is prefixed with ', so a spreadsheet does
not run it as a formula.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:
options(coreval.progress = TRUE) or
FALSE.read_study() and pass the object to
check_study().version cuts the number of rules further than
standard alone, and with them the time.usdm = TRUE) because a few of
their rules can take minutes on one file.| 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.
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.
Three programs run these rules:
cdisc-org/cdisc-rules-engine), written in Python. Since
CORE 1.0 it is the reference for SEND, TIG and USDM.verisianHQ/cdisc-rules-engine), the CORE 1.0 engine,
written in Python on a PostgreSQL database. It is the reference for
SDTMIG and the FDA Business 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.
legacy_ids column of list_rules() gives the
CDISC Conformance Rules and FDA Business Rules ids, which is how to
match a finding to a severity in another tool.Status and Notes columns of a saved file, and
Owner in a CSV, are for tracking by hand.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.