First release. The API is settled for 0.1.x; anything that changes will go through a deprecation cycle rather than disappearing.
A personal open-source project — not affiliated with or endorsed by CDISC, and not a CORE-certified engine. It’s a quick local check to run before your qualified validation tool, never instead of it.
check_dataset()
— a data frame you already have open, or a single .xpt,
.sas7bdat or .csv file. No study folder
needed. This is the one for when you’re mid-way through writing the code
that builds a domain. coreval works out the domain from your
DOMAIN column, or the file name. Rules that need a dataset
you didn’t supply are skipped and say so, rather than being run against
columns that aren’t there.read_study()
on a folder, then check_study(). It reads XPT, SAS and CSV,
and picks up Define-XML (2.0 or 2.1) if it’s there. Reading everything
at once is what makes the cross-dataset rules work.$findings as an issue column, so a rule number
is never the only thing you get.wrong value (your data breaks the rule:
a month of 13, a value outside its codelist) from
missing required and missing optional (often
legitimate — a screen-failure subject, a variable your raw data does not
carry yet). Sorting by row count alone put those in the wrong order. It
is a triage column on every finding, so a spreadsheet can
be sorted by it too.print(result, n = 20, rows = 5) shows more.missing required variables: SUBJID, SITEID, COUNTRY.write_findings() to Excel or CSV. You get
findings, skipped, an about sheet
and - when a rule matched more records than were kept -
truncated. The file carries empty Status,
Owner and Notes columns for you to fill in, so
“expected, see protocol deviation log” lives next to the finding instead
of in another document. Pass tracking = FALSE to leave
those out.about records which standard the run was scoped to, how
many checks ran, whether the result was filtered before export, and
whether any counts were capped. A shared spreadsheet outlives the
console session that made it, and whoever opens it cannot otherwise tell
that it is partial.CG0665,
SEND66, TIG0699, FB0801 - which
are the ids P21 and the published Conformance Rules spreadsheets use.
The report shows them next to the CORE id, so a finding here can be
matched to a finding there, including to a severity CDISC itself does
not publish. All 797 rules have at least one.print(result, guidance = TRUE) shows it under
each problem - off by default, since it roughly doubles the report - and
list_rules() always returns it.list_rules() now answers all three:
list_rules() for the catalog,
list_rules(id = "CORE-000547") to look up a rule the report
named, list_rules(domain = "AE") for what applies to a
domain. The columns are the same whatever you ask, so the result is safe
to filter, join and script against. rule_info(),
rules_for_domain() and rules_version() are
gone - the last is now attr(list_rules(), "rules_version"),
and write_findings() records it in every exported file
anyway.summary() gives the counts in three
lines, for a script or a quick “did that fix help?”, and returns them as
a row you can log.filter_findings() narrows a result by
triage, dataset, rule or variable, and returns a result - so it still
prints as a readable report, and says it is a subset rather than passing
for the whole picture.Checking is dramatically faster - roughly 50x on
large data. Four things were doing per-row work on whole columns: the
date operators called grepl()/regexpr() once
per value; findings were assembled one data.table per
violating record; the code building reported values re-decided what kind
of thing each variable was for every row; and the uniqueness operators
answered “does this key repeat?” by building an interaction factor and
sorting it, rather than by hashing.
| rows | before | after |
|---|---|---|
| 10 000 | 37 s | 1.1 s |
| 200 000 | ~12 min | 18 s |
| 1 000 000 | ~1 hour | 71 s |
Finding counts are identical at every size.
Findings are capped at 1000 records per rule by
default. A rule can flag every row - a missing EPOCH on a
200 000-row LB is 200 000 identical findings, beyond what
anyone reads or Excel can hold. The true count is kept in the new
truncated table and shown in the report (“1 000 000 records
(first 1 000 kept)”), so nothing is under-reported.
max_records = Inf keeps everything.
A progress bar appears for long checks when running
interactively, so a slow study no longer looks like a hang.
options(coreval.progress = FALSE) turns it off; it is
already off in scripts.
'nzchar()' requires a character vector.
read.csv(stringsAsFactors = TRUE) and plenty of older code
still produce factors, so they are converted rather than refused.DOMAIN no longer changes
the answer. "AE " was treated as a domain of that
name: it scoped to a different rule set, and resolved
"--STDTC" to "AE STDTC" - a column nothing has
- so every "--" rule silently found nothing. (The padded
value is still reported as a problem in its own right, by the rules that
exist to catch exactly that.)DOMAIN,
says which it is. The old message claimed there was “no single
DOMAIN value”, which reads as “your column is inconsistent” to someone
whose data simply has no rows yet.days_in_month() was wrong for vector input: it built
its lookup table with c(31, ifelse(leap, 29, 28), 31, ...),
which produces one element per YEAR rather than one per month, so for n
years the table was 11 + n long and every month from March on read the
wrong slot. Correct for a single value and wrong for a column - which
the old per-row date code hid completely. "2003-11-31" was
rejected when checked alone and accepted when checked as part of a
column.standard = "SDTMIG" was accepted and silently ignored
before, so an SDTM study was also measured against SENDIG rules - 73 of
the 270 rules in scope for DM are SENDIG-only. Declaring it now halves
the rules and more than halves the reported problems, losing no real
coverage. Matched exactly, so a SENDIG study does not pick
up SENDIG-DART rules.check_dataset(dm, standard = "SDTMIG", version = "3.4").
Rules are written per version - 408 SDTMIG rules exist for 3.2 against
445 for 3.4, and 86 apply to exactly one version - so without this a 3.2
study is measured against rules written for a guide it does not follow.
"3-4" and "3.4" are both accepted, since CORE
test cases write the first and the rules the second.include_deprecated = TRUE. A deprecated rule
has a published replacement, so running both reports the same defect
twice.inst/COPYRIGHTS. cdisc-open-rules
is MIT licensed, and MIT requires the copyright and permission notice to
accompany substantial portions of the work - coreval bundles 797
extracted rules and CDISC standards metadata, but the notice lived only
in NOTICE.md, which is excluded from the build and so never
reached anyone who installed the package. A test now guards it.check_study() takes a folder path, so
read_study() is now optional:
check_study("study/sdtm") instead of reading first. Reading
yourself is still worth it to inspect what was parsed, or to check the
same large study twice without re-reading it.sdtm_domain_classes() is no longer exported - a
domain-to-class lookup table answers a question nobody working with
their own data has.evaluate_rule() is no longer exported. It returned a
raw logical vector, needed a rule record fetched from package internals,
and had no story a user could follow now that
check_dataset() exists.Unpublished/SDTMIG
and Unpublished/SENDIG - folders the extractor never
opened. Both are SDTM- and SEND-shaped, so this engine can read their
data, and only the ones CDISC already ships expected results for are
taken (11 of 128, and 30 of 77). That is the whole remaining gap: of the
767 upstream rules in a readable format that CDISC ships results for,
coreval now has 767.read_study()
cannot read it at all. Unpublished/ADAMIG (93) ships
test data but no expected results for a single rule, so nothing
there can be verified. Another 292 readable rules have no expected
results either.contains_case_insensitive and
does_not_contain_case_insensitive.TO, PT, IN, RELREF
and nine others resolved to nothing. Every rule scoped to one failed its
Scope > Classes check and was skipped as “no dataset
matches the rule’s scope” - with the right dataset sitting in the test
case. Taken from the same CDISC cache as the rest of the table, and
checked against the 42 shared domains first: no conflicts.2024-03), grouping and uniqueness checks, and set
membership.Operations pipeline that pre-computes values rules refer
to.0.0 comes back as 0.