DEV Community

TECHWORKSLAB
TECHWORKSLAB

Posted on Originally published at techworkslab.com

Writing a Pinnacle 21 style check in R, and why the check is not the evidence

Conformance checking feels like the end of the job. You run the validator, the report comes back clean or close to it, and the dataset is pronounced ready. We want to make a narrower and more useful point: the check is trivial to write, the running of it is where all the value is, and a passing check on its own is worth almost nothing as evidence. To make the first part concrete, here is a small conformance check in R. To make the second part concrete, here is what it is missing.

A check is just a function that returns findings

A conformance rule is a question asked of a dataset that returns the rows where the answer is no. That is it. Here are three rules that cover a surprising amount of real world trouble: a required variable is missing, a key is not unique, and a date is not valid ISO 8601. Each returns a tidy table of findings, and the empty table is the pass.

library(dplyr)
library(tibble)

# Rule 1: every required variable for the domain is present
check_required_vars <- function(data, domain, required) {
  missing <- setdiff(required, names(data))
  tibble(
    domain   = domain,
    rule     = "required-variable-present",
    severity = "error",
    detail   = missing
  )
}

# Rule 2: the key variables are unique (one row per key)
check_unique_key <- function(data, domain, key) {
  dup <- data %>% count(across(all_of(key))) %>% filter(n > 1)
  tibble(
    domain   = domain,
    rule     = "unique-key",
    severity = "error",
    detail   = if (nrow(dup) == 0) character(0)
               else paste(nrow(dup), "duplicated key value(s)")
  )
}

# Rule 3: --DTC values are ISO 8601 date or datetime (allowing partials)
check_iso8601 <- function(data, domain, dtc_var) {
  iso <- "^\\d{4}(-\\d{2}(-\\d{2}(T\\d{2}:\\d{2}(:\\d{2})?)?)?)?$"
  vals <- data[[dtc_var]]
  bad  <- vals[!is.na(vals) & nzchar(vals) & !grepl(iso, vals)]
  tibble(
    domain   = domain,
    rule     = paste0("iso8601-", dtc_var),
    severity = "error",
    detail   = unique(bad)
  )
}
Enter fullscreen mode Exit fullscreen mode

Three rules, each a plain function returning a findings tibble. The pattern scales to as many rules as you care to write.

Running them is a matter of binding the findings together. A real validator does this across every domain and hundreds of rules, but the shape is the same.

# A tiny AE-like frame to check
ae <- tibble(
  STUDYID  = "PILOT01",
  USUBJID  = c("01-001", "01-001", "01-002"),
  AESEQ    = c(1, 1, 1),                 # note: duplicate key on subject 01-001
  AESTDTC  = c("2026-03-01", "2026-14-01", "2026-03-05")  # note: month 14
)

findings <- bind_rows(
  check_required_vars(ae, "AE", c("STUDYID", "USUBJID", "AESEQ", "AETERM")),
  check_unique_key(ae, "AE", c("USUBJID", "AESEQ")),
  check_iso8601(ae, "AE", "AESTDTC")
) %>% filter(nzchar(detail) | lengths(detail) > 0)

findings
#> # A tibble: 3 x 4
#>   domain rule                      severity detail
#>   <chr>  <chr>                     <chr>    <chr>
#> 1 AE     required-variable-present error    AETERM
#> 2 AE     unique-key                error    1 duplicated key value(s)
#> 3 AE     iso8601-AESTDTC           error    2026-14-01
Enter fullscreen mode Exit fullscreen mode

The findings table is the whole output. Writing more rules is more of the same, which is exactly why the rules are not the hard part.

You could spend a week turning this into something that reads real SDTM, loads a rule pack, and formats a report, and it would be useful work. But notice what you would have built: a program that, given a dataset, tells you which rows it does not like today. That is necessary. It is nowhere near sufficient.

The findings table is an output. The evidence is everything that lets someone reproduce it.

Why the check is not the evidence

Picture the clean report, printed, in a binder, a year later. It says zero errors. Now ask the questions an auditor asks. Which version of the dataset did it run on. Which rule set, at which version, with which thresholds. Who ran it, and on what machine. Could someone re-run it today and get the same report. A bare report answers none of these. It is a screenshot of a moment, and it can be a screenshot of a moment that no longer exists.

The evidence is not the green tick. The evidence is the recorded run: the exact dataset it saw, identified by a hash, not a filename; the exact rule pack and its version; the person who ran it, established through authenticated sign on; the environment it ran in; and a timestamp that cannot be backdated. Capture those and the report becomes reproducible and therefore defensible. Omit them and the report is an assertion, and we have written before about why an assertion of oversight is not oversight.

Anyone can produce a clean conformance report. The question that matters is whether someone else can reproduce yours, exactly, and show who stood behind it.

What we actually build around the check

So the check is the small part, and the record around it is the system. In our platform the run is the unit that is captured, not the result. Each conformance run records the dataset it read by content hash, the rule pack and version it applied, the identity of the operator through single sign on, and the environment it executed in. The whole sequence lands in a tamper evident, hash chained audit trail, so the order of events and who caused them cannot be quietly rewritten afterwards. The findings table from the check is one field in that record, not the record itself.

This is also why we keep saying you should validate the artifact, not the author. It does not matter whether a person, a script, or an assistant produced the dataset. What you validate is the dataset, and what you keep is the evidence of the validation, bound to the exact thing that was validated. The rule in R took ten lines. The discipline around running it is the part that holds up under inspection.

If you are building conformance into a pipeline and want it to produce evidence rather than just reports, we are happy to talk it through. You can reach us here.

Top comments (0)