Runs the full custody chain over a capsule directory in one call and reports, check by check, whether the capsule is still the one the analysis was run on: the provenance record is readable, the pinned data file is present and has its recorded SHA-256, the schema still validates, a recorded analysis script still has its pinned hash, every numeric cross-check in a results manifest still reproduces its recorded verdict, and the data are real rather than synthetic. Use it before re-running or publishing an analysis, or when receiving a capsule from someone else.
Usage
vrfcps(
cpsdir,
provfile = "data_provenance.json",
dfile = NULL,
mnffile = NULL,
scrfile = NULL
)Arguments
- cpsdir
Directory containing the capsule, a single string.
- provfile
Provenance JSON filename, relative to
cpsdir."data_provenance.json"is the default.- dfile
Data filename relative to
cpsdir.NULL(the default) uses the provenance'sresource$filename.- mnffile
Optional results-manifest JSON (as written by
wrmnfjs()), relative tocpsdir.NULL(the default) usesmanifest.jsonwhen the capsule has one.- scrfile
Optional analysis-script filename relative to
cpsdir; compared against the pinned script digest.NULL(the default) uses the script named in the provenance or the manifest, if any.
Value
An object of class c("bricklayer_capsule_check", "list"),
printed as one line per check:
oklogical scalar, the overall verdict described in Details.
checksdata.frame with columns
check(character, the check name),ok(logical) anddetail(character: the path checked, the digest found, or why it failed).requiredcharacter, the checks that had to be made.
missingcharacter, required checks that were never made (
character(0)when none).
Details
The checks, in the order they appear in checks:
provenance_readableprovfileis a plain relative path inside the capsule (no.., no absolute path, no symlink out) and parses as JSON.data_presentthe data file (
dfile, else the provenance'sresource$filename) is inside the capsule and is a file.data_sha256vrfsha256()finds that its SHA-256 equalsresource$sha256(64 hex characters).data_size_byteswhen
resource$size_bytesis recorded: the size on disk matches.data_readablea
.csvfile is read withutils::read.csv(); a non-CSV file fails this row when a row count or schema is recorded (they cannot be checked).data_row_countwhen
resource$row_count_data_rowsis recorded: the CSV has that many rows.schema_validwhen a
schemais recorded:valsch()finds no fatal issue (warnings are listed indetailbut pass).manifest_consistentwhen
mnffileis given, or the capsule holdsmanifest.json: every result with numericobservedandexpected, atoland a status ofPASSorDIFFERstill gets that status from \(|observed - expected| \le tol\).script_sha256when a script is named (by
scrfile, byscript$filenamein the provenance or bymeta$scrfilein the manifest) or a script digest is pinned (meta$script_sha256orscript$sha256): the script's SHA-256 matches the pin.data_not_syntheticno
<data>.syntheticsidecar (written bymksyncsv()), and neither the manifest nor the provenance recordssynthetic = true.
A check that cannot be made because the
provenance lacks a field is a FAILED row, never skipped: a provenance of
{} verifies nothing. ok is TRUE only when every row
passed and every required check was made. The required checks are
provenance_readable, data_present, data_sha256 and
data_not_synthetic, plus manifest_consistent and
script_sha256 when a manifest or a script is in play.
Entirely offline: nothing is downloaded and nothing is written. Error:
cpsdir that is not one existing directory. Every other problem
is a failed row, not an error.
Examples
dir <- file.path(tempdir(), "capsule-example")
dir.create(dir, showWarnings = FALSE)
write.csv(data.frame(id = 1:3), file.path(dir, "d.csv"), row.names = FALSE)
prov <- list(resource = list(filename = "d.csv",
sha256 = sha256file(file.path(dir, "d.csv")),
row_count_data_rows = 3),
schema = list(expected_columns = "id"))
writeLines(tojs(prov, auto_unbox = TRUE),
file.path(dir, "data_provenance.json"))
vrfcps(dir)$ok
#> [1] TRUE
vrfcps(dir)
#> ╭── Capsule verification ─────────────────────────────────────────────────────╮
#> │ ✔ VERIFIED · 7 of 7 checks passed │
#> │ │
#> │ Status intact │
#> │ Checks 7 of 7 passed │
#> │ │
#> │ Every check passed: the capsule is still the one the analysis was run on. │
#> │ │
#> │ ✔ provenance_readable /tmp/RtmpZpRdcE/capsule-example/data_provenance │
#> │ .json │
#> │ ✔ data_present /tmp/RtmpZpRdcE/capsule-example/d.csv │
#> │ ✔ data_sha256 da4aaa3a009d063f36d0515f2b50a11bcc578264aa7c455 │
#> │ 04b46fedbbae4f8f7 │
#> │ ✔ data_readable /tmp/RtmpZpRdcE/capsule-example/d.csv │
#> │ ✔ data_row_count 3 rows on disk │
#> │ ✔ schema_valid │
#> │ ✔ data_not_synthetic no synthetic marker: sidecar, manifest and │
#> │ provenance all say real data │
#> ╰─────────────────────────────────────────────────────────────────────────────╯
# an edited data file fails its digest and its row count
write.csv(data.frame(id = 1:4), file.path(dir, "d.csv"), row.names = FALSE)
chk <- vrfcps(dir)
chk$ok
#> [1] FALSE
chk$checks[!chk$checks$ok, c("check", "detail")]
#> check
#> 3 data_sha256
#> 5 data_row_count
#> detail
#> 3 expected da4aaa3a009d063f36d0515f2b50a11bcc578264aa7c45504b46fedbbae4f8f7, got b24bb2c082053f4d1f5db97140babb86d4cc7e31b49be4e3c4cf94ca87a86d2b
#> 5 4 rows on disk
# a path out of the capsule is a failed check, not a read
vrfcps(dir, provfile = "../elsewhere.json")$ok
#> [1] FALSE
# a missing directory is an error
try(vrfcps(file.path(tempdir(), "no-such-capsule")))
#> Error : `cpsdir` is not an existing directory: /tmp/RtmpZpRdcE/no-such-capsule
unlink(dir, recursive = TRUE)
