Generate the validation evidence for an R environment, on your own machine.

Open-source clinical R packages are free. The evidence that they are fit for regulated use is not — that is what Atorus OpenVal and Posit Package Manager’s validation tier are sold for. valpack produces the same class of artefact locally, from the library you actually have installed:

installed library -> IQ record + OQ record + risk assessment + manifest -> evidence bundle
Artefact Function What it proves
Risk assessment assess_risk() Each package’s riskmetric score and every underlying metric
Installation qualification iq_record() What is installed: version, library, built-under R, provenance, DESCRIPTION checksum
Operational qualification oq_record() The package was exercised and here is what happened, per unit, with the real failure text
Environment manifest env_manifest() A JSON snapshot another site can diff against theirs
Evidence bundle evidence_bundle() All four plus a linked index.html for the reviewer

Nothing is inferred and nothing is assumed to pass: every OQ row is the observed outcome of running real code in a clean R subprocess.

Install

install.packages(
  "https://clincoder.cloud/valpack/valpack_0.0.0.9001.tar.gz",
  repos = NULL, type = "source"
)

Worked example

library(valpack)

pkgs <- c("whisker", "jsonlite")

# What is installed, and is it still the file we qualified?
iq <- iq_record(pkgs)
iq$packages[, c("package", "version", "source", "description_md5")]

# Exercise it. `types = "tests"` also runs the suite when one is installed.
oq <- oq_record("whisker", types = c("examples", "tests"))
oq$summary
subset(oq$results, status == "fail")[, c("unit", "message")]

# Risk. The default assessments query CRAN; this subset is offline and fast.
quick <- riskmetric::all_assessments()[c(
  "assess_has_news", "assess_has_vignettes", "assess_has_examples",
  "assess_exported_namespace", "assess_has_maintainer"
)]
risk <- assess_risk(pkgs, assessments = quick)
risk[, c("package", "version", "score")]

# Hand the reviewer one directory.
evidence_bundle(iq, oq, risk, path = "evidence")
#> v Evidence bundle written to .../evidence

Real output from the run above on this machine (whisker 0.4.1, R 4.5.2):

# A tibble: 2 x 7
  package type      pass  fail  skip harness elapsed
  <chr>   <chr>    <int> <int> <int>   <int>   <dbl>
1 whisker examples     4     0     5       0    0.02
2 whisker tests        5     2     0       0    2.74

Those two failures are genuine: whisker 0.4.1 fails two mustache-spec test files, and oq$results$message carries the testthat diff that proves it. A validation tool that reported 7/7 there would be worthless.

Scope: what the full-library run does and does not qualify

oq_record() has been run across the whole installed library. That is the scale claim; here it is stated precisely, with the limits attached.

These are the numbers of the pre-fix 2026-08-02 run, kept as measured because they are cited elsewhere. All 140 of its failures were adjudicated on 2026-08-03 by re-executing every one of them: 92 (65.7%) were artefacts of valpack’s own harness, 20 were this machine’s environment, 17 were one package hitting the operator’s 300 s budget, and 7 (5.0%) were genuine package defects. The harness classes are fixed in this version — see “The harness was the largest defect” below — but no corrected full re-run has been published, so nothing here supersedes the figures in this table.

2026-08-02 23:25 UTC · R 4.5.2 · x86_64-pc-linux-gnu · Ubuntu 26.04 LTS · 580 packages · per-package timeout 300 s · 120.1 min of subprocess wall time.

Units passed 14,554
Units failed 140 (0.95% of the 14,694 executed)
Units skipped 8,765 (37.4% of all 23,459 units)
Packages with at least one failing unit 41
Packages with zero executed units 32

Limits that travel with that number

  • Examples only. No test suites were run. The run collected types = "examples"; there is not one tests row in it. Asking for tests would have changed little: only 7 of the 580 installed packages ship an installed tests/ directory, because the library was not installed with --install-tests. So “OQ across the library” means “every package’s help-page examples were executed” — materially weaker than it sounds. iq_record() now records installed_tests per package and env_manifest() reports the library-wide count, so the fact ships with the evidence instead of living in a README.
  • 37.4% of units were skips — an example with no runnable code (all inside \dontrun{}), or one that guards itself against an absent suggested package. A skip is honest, but it is not qualification.
  • 32 packages had no executed unit at all, so nothing about them was qualified: AsioHeaders, BH, clipr, devtools, extrafontdb, filehash, filelock, foghorn, fontBitstreamVera, fontLiberation, pharmaversesdtm, pharmaverseraw and 20 more — header-only, data-only and meta-packages. Full list in the bundle’s zero_coverage_packages.csv.
  • One machine, one date, one R version. None of it transfers to a different library or a different R.
  • riskmetric — valpack’s own hard Imports — was qualified on zero executed units. 108 topics: 63 have no \examples section at all and 45 are entirely \dontrun/\donttest. assess_risk() and every evidence_bundle() risk page run through riskmetric::pkg_ref()pkg_assess()pkg_score(), so the tool that produces this evidence has a hard dependency this OQ says nothing about. It is exercised by valpack’s own testthat suite (tests/testthat/test-risk.R, test-bundle.R), which is not part of any types = "examples" run. State that in the validation report; no code change here can close it.
  • A pass is reachability, not correctness. No expected output was compared and nothing was checked against a reference. An example that runs proves the code path is reachable and does not abort — nothing about whether the numbers it produced are right.

The harness was the largest defect (fixed in this version)

Adjudication of all 140 failures found 92 caused by the harness itself. valpack refuses to use tools::testInstalledPackage() because it reports false failures, and had then reproduced the same class of defect in its own runner. Each class below is fixed, and each fix has a regression test that fails without it (tests/testthat/test-oq.R).

Class False failures Cause Fix
H3 52 No cleanEx() between topics, so one topic’s objects shadowed the next topic’s data (all 6 Tplyr, 13 mgcv, 7 huxtable) The global environment is emptied, attached packages detached, RNG reseeded, warn and the working directory restored between topics — what R CMD check does
H1 29 An example rebound a base function the runner itself called (DHARMa) All harness state lives in an environment parented at baseenv(), and the driving loop runs inside it
H4 7 source() defaults print.eval to FALSE, so lazily-drawn objects were never drawn (Hmisc, lattice) print.eval = TRUE on a real pdf() device, as the REPL and R CMD check do
H7 2 progressr refuses to run while condition handlers are on the stack, and the isolating tryCatch is one Recorded harness, never fail
H5 1 options(keep.source = TRUE) set inside a file source() has already parsed (utils::removeSource) keep.source = TRUE set before parsing
H6 1 Example needs the REPL’s .Last.value (MASS::renumerate) Detected and refused as harnessno batch harness can close this one
H2 17 One slow robmixglm topic hit the 300 s per-package budget and condemned its 16 siblings Results are checkpointed after every unit; the timeout is recorded in $session$timeout; cut-short units are harness, never fail
H8 14 Packages phrasing their own missing-Suggests guard in wording the skip regex did not match Regex widened to the wrapped phrasings

Measured on the packages the adjudication named, before and after, same machine, same day, timeout = 300:

Package fail before fail after
Tplyr 6 0
mgcv 13 0
huxtable 7 0
Hmisc 3 0
psych 4 0
total 33 0

Passes on those five went from 524 to 557; skips were unchanged at 173.

The other named packages, measured the same way (before = the 2026-08-03 re-run under the previous harness): DHARMa 5 → 0, lattice 2 → 0, latticeExtra 1 → 0, rsvg 1 → 0, utils 1 → 0, tidycmprsk 1 → 0 (now a skip), metafor 3 → 0 (now skips), MASS 1 → 0 fail + 1 harness, progressr 2 → 0 fail + 2 harness, and robmixglm 17 fail → 3 pass, 0 fail, 14 harness — the budget cut it short, which is not a property of the package and is no longer recorded as one.

A unit that could not be run is not a pass and not a failure

status now has four values, and the fourth exists because a validation tool must never let “we could not run this” read as either:

Status Meaning
pass Ran to completion without erroring
fail Ran and errored — the only status that points at the package
skip Nothing to run (\dontrun{}), or the example guarded itself against an absent suggested package, as CRAN does with _R_CHECK_FORCE_SUGGESTS_=false
harness The harness produced no verdict: refused (needs the REPL, or the package refuses condition handlers), cut short at timeout, or never reached because the subprocess ended early. message says which. Missing coverage, not a defect

Failure taxonomy — the 140 failures by cause, as recorded pre-fix

A bare failure count is not reviewable. evidence_bundle() groups every failing unit by the cause its message evidences:

Cause Failures Packages
error in example 78 29
subprocess did not complete 46 2
missing suggested package 13 9
absent external resource 2 2
network 1 1
  • subprocess did not complete (46) is missing coverage, not defect — the units were never judged, which is why such units now carry status harness rather than fail. 17 are robmixglm hitting the 300 s per-package timeout. The other 29 are DHARMa 0.4.7, and they were valpack’s own bug: a DHARMa example bound c in the global environment and the harness, which resolved c there too, died. Fixed in this version (the runner’s own names resolve from baseenv()) and covered by a regression test. A targeted re-run of DHARMa alone under the fix records 24 pass / 5 fail / 23 skip instead of 0 / 29 / 23. The 140 above is what the 2026-08-02 run measured and has not been restated — a corrected full re-run would report fewer.
  • missing suggested package (13, measured 14 on adjudication) are false failures too. The subprocess turns “there is no package called X” into a skip, as CRAN does with _R_CHECK_FORCE_SUGGESTS_=false, but these packages phrase their own guard differently (“Please install the ggdist package…”, The package "aod" (>= 1.3.3) is required.) and were recorded as failures. Now reclassified: the regex covers the wrapped phrasings, with a regression test. The residual risk — a genuine defect whose message says “Please install X” — is real but smaller than booking 14 environment gaps as package failures.
  • error in example (78) meant the example errored as run here — in Rd order, sharing one global environment. Adjudication re-ran every one: 52 of them were the missing cleanEx() and pass in isolation, 7 were the undrawn-plot bug, 20 were this machine’s environment, and 7 were genuine package defects (5 of them one cardx/cards version mismatch). The order-dependence is gone; the environment ones remain environment ones.

Bundle: /root/evidence/valpack_full_oq/bundle/index.html, oq.html (taxonomy), failure_taxonomy.csv, zero_coverage_packages.csv. Re-publish with Rscript data-raw/publish_full_oq.R.

Notes for the reviewer

  • score is riskmetric’s roll-up on 0-1, higher is better. Scores from different assessment sets are not comparable, in either direction. Measured here (R 4.5.2, riskmetric 0.2.7): tibble scores 0.1480 on a five-metric subset and 0.3478 on all nineteen — but whisker scores higher on two metrics (0.5000) than on all nineteen (0.4600), so “a subset deflates the score” is not a safe rule either. The tibble carries an assessments attribute, print() shows it, and the bundle’s risk page states it, so a mismatch is detectable rather than a matter of memory.
  • oq_record() reports only on the types it was asked for and records that request in $session$types. It does not emit a tests row when tests were never requested — reporting on something that was not attempted would be fabrication — so $session$types is what tells a reviewer the scope.
  • Base packages have no repository URL. valpack records NA rather than inventing one.
  • No renv dependency: the manifest describes what is on disk, not what a lockfile says should be.

License

MIT (c) Bhanoji Duppada