
Record a recode as a manifest that can be signed and verified later
Source:R/categorical_guard.R
recode_manifest.RdBuilds a record of a recode that a later reader can check: the mapping
and its SHA-256, the counts per label before and after, the results of the
before/after cross-tabulation check and (when given) of the match against
published counts, a UTC timestamp, and, with a key, a signature over the
whole record. Write it to disk with write_recode_manifest
and re-check it with verify_recode_manifest, so anyone can
confirm which labels went where and that nothing was changed afterwards.
Arguments
- original, recoded
Vectors of the same length: the values before and after the recode.
- mapping
Named character vector, old = new: the mapping that was applied.
- published
Optional named numeric vector of published counts per recoded label, default
NULL(no check).- key
Optional signing key, default
NULL(unsigned): a key frompqc_keygenorfips_keygen. When given the manifest carries a signature and the public key.- context
Optional character string, default
NULL: free text such as the dataset, table and analyst. Only the first element is kept.
Value
A list of class c("bricklayer_recode_manifest", "list")
with elements context, created (ISO 8601 UTC time),
mapping (list), mapping_sha256, n (number of rows),
n_missing, counts_before and counts_after (named
lists of counts), checks (list of crosstab_is_function,
missingness_unchanged, matches_published),
published (list or NULL), body_sha256, and with a
key signature and public_key.
Details
The function first runs verify_recode(original, recoded,
mapping) and, when published is given,
verify_marginals(recoded, published) with the default
strict = TRUE. Either check failing is an error, so a manifest is
only produced for a recode that passed; checks$matches_published is
therefore TRUE or NA (no published counts), never
FALSE.
body_sha256 is core_sha256 of the canonical JSON of
every other field. With a key, capsule_sign signs that
hash and the manifest also stores the public key: a
bricklayer_signing_key from pqc_keygen (XMSS, which
consumes one one-time leaf per signature) or a key from
fips_keygen (ML-DSA or SLH-DSA). Nothing is written to disk.
Examples
x <- c("W", "B", "W", "I")
map <- c(W = "White", B = "Black", I = "Indigenous")
y <- guard_recode(x, map)
m <- recode_manifest(x, y, map,
published = c(White = 2, Black = 1, Indigenous = 1),
context = "example survey, race item"
)
m$checks
#> $crosstab_is_function
#> [1] TRUE
#>
#> $missingness_unchanged
#> [1] TRUE
#>
#> $matches_published
#> [1] TRUE
#>
m$counts_after
#> $Black
#> [1] 1
#>
#> $Indigenous
#> [1] 1
#>
#> $White
#> [1] 2
#>
# signed with an ML-DSA key
k <- fips_keygen(seed = as.raw(1:32))
ms <- recode_manifest(x, y, map, key = k)
names(ms)
#> [1] "context" "created" "mapping" "mapping_sha256"
#> [5] "n" "n_missing" "counts_before" "counts_after"
#> [9] "checks" "published" "body_sha256" "signature"
#> [13] "public_key"
# a recode that does not match the published counts is refused
try(recode_manifest(x, y, map, published = c(White = 1, Black = 2, Indigenous = 1)))
#> Error : verify_marginals: recoded counts do not match the published counts. White: observed 2, published 1; Black: observed 1, published 2; Indigenous: observed 1, published 1. The observed counts match the published ones if the labels are permuted (White -> Black, Black -> White): the labels are attached to the wrong groups.