reviewerguider populates the mechanical sections of a Study Data or Analysis Data Reviewer’s Guide directly from dataset metadata, validation findings and a programs directory – leaving only the genuinely narrative sections to be written by hand.

Scope

Generated mechanically: the dataset inventory, the validation-findings table, the programs-submitted list, and the assembly of the document itself.

Left to you, deliberately: every narrative section (study background, rationale for non-standard decisions), and every sponsor explanation of a finding. Findings with no explanation are only flagged (needs_explanation = TRUE); the package never writes an explanation for you. The generated content is a draft from your metadata – read it before it ships.

Two values are guessed rather than known, and both are disclosed where you can see them: key variables when the study supplies no declared keys (the key_source column says "heuristic"), and program descriptions, which are the header comment rather than an analysis of the code. Both limits are spelled out in the sections below and in ?section_datasets / ?section_programs.

1. Start a guide

library(reviewerguider)

guide <- guide_spec(
  "CDISCPILOT01",
  type = "sdrg",
  sponsor = "Acme Pharma",
  protocol_title = "A Study of Drug X",
  standards_version = "SDTMIG 3.4"
)
guide
#> 
#> ── CDISCPILOT01 reviewer's guide (SDRG) ────────────────────────────────────────
#>  Sponsor: Acme Pharma
#>  0 section(s)

2. Dataset inventory

section_datasets() builds the inventory from a named list of data frames (or an already-assembled metadata table):

adsl <- read.csv(system.file("extdata", "adsl.csv", package = "reviewerguider"))
adae <- read.csv(system.file("extdata", "adae.csv", package = "reviewerguider"))
section_datasets(list(adsl = adsl, adae = adae))
#> # A tibble: 2 × 8
#>   dataset label class      key_variables      key_source records variables size 
#>   <chr>   <chr> <chr>      <chr>              <chr>        <int>     <int> <chr>
#> 1 adsl    NA    data.frame STUDYID, USUBJID,… heuristic        6         7 2.7 …
#> 2 adae    NA    data.frame STUDYID, USUBJID,… heuristic        6         5 2.4 …

With no metadata, key_variables is a name-pattern guess and key_source says so. Where the study declares its keys – a metacore object from metacore::define_to_metacore(), or any data frame with dataset, variable and key_seq columns – pass it and the declared keys are used instead:

keys <- read.csv(system.file("extdata", "dataset_keys.csv", package = "reviewerguider"))
head(keys)
#>   dataset variable key_seq
#> 1    ADSL  USUBJID       1
#> 2    ADAE  USUBJID       1
#> 3    ADAE   AETERM       2
#> 4    ADAE    ASTDT       3
#> 5    ADAE    AESEQ       4
#> 6   ADLBC  USUBJID       1
section_datasets(list(adsl = adsl, adae = adae), metadata = keys)
#> # A tibble: 2 × 8
#>   dataset label class      key_variables      key_source records variables size 
#>   <chr>   <chr> <chr>      <chr>              <chr>        <int>     <int> <chr>
#> 1 adsl    NA    data.frame USUBJID            declared         6         7 2.7 …
#> 2 adae    NA    data.frame USUBJID, AETERM, … declared         6         5 2.4 …

The declared keys (from the real CDISCPILOT01 ADaM define.xml) disagree with the guess for both datasets – which is the point of never letting a guide guess silently. Read key_source before the table goes into a guide.

3. Validation findings

section_findings() takes a plain data frame in the Pinnacle 21 report column schema – Dataset, Variable, Rule ID, Message, Records affected, Severity – so any tool that emits those columns works, and no validator package is imported. Names are matched case- and separator-insensitively, and a missing required column is an error naming it.

It pairs each finding with its sponsor explanation (keyed by rule ID, or by message text) and flags the ones still missing one – that gap is exactly what gets a submission questioned. An unexplained finding is only flagged, never filled in:

findings <- read.csv(system.file("extdata", "findings.csv", package = "reviewerguider"))
explanations <- read.csv(system.file("extdata", "explanations.csv", package = "reviewerguider"))
section_findings(findings, explanations)
#> ! 3 finding(s) have no sponsor explanation and are flagged.
#> # A tibble: 6 × 8
#>   dataset   variable       rule_id message records_affected severity explanation
#>   <chr>     <chr>          <chr>   <chr>              <int> <chr>    <chr>      
#> 1 LB (LBHE) LBTEST, LBSTR… SD0029  Missin…             1000 Warning  NA         
#> 2 AE        AEENDTC        SD0021  Missin…              472 Warning  Adverse ev…
#> 3 LB (LBHE) LBORRES, LBTE… SD0026  Missin…              293 Warning  Original u…
#> 4 LB (LBUR) LBORRES, LBTE… SD0026  Missin…               25 Warning  Original u…
#> 5 DM        ARMCD, ACTARM… SD2236  ACTARM…               12 Warning  NA         
#> 6 QS (QSCO) QSSTRESC, QSB… SD1131  Missin…                7 Warning  NA         
#> # ℹ 1 more variable: needs_explanation <lgl>

4. Programs submitted

section_programs() inventories a directory of submission programs, pulling each program’s leading comment block as its description:

progs <- system.file("extdata", "programs", package = "reviewerguider")
section_programs(progs)
#> # A tibble: 2 × 5
#>   file     path     size  modified            description                       
#>   <chr>    <chr>    <chr> <dttm>              <chr>                             
#> 1 adsl.R   adsl.R   231 B 2026-08-03 02:29:55 Program: adsl.R Purpose: Derive t…
#> 2 t_dm.sas t_dm.sas 211 B 2026-08-03 02:29:55 Program: t_dm.sas Purpose: Produc…

description is the header comment (#, * or /* */) found in the first 60 lines, collapsed to one line and truncated at 300 characters – not a parse of the code. A header sitting below a licence banner or an options/library block gives NA, and a stale header is reported verbatim. Overwrite the column if you need better.

5. Assemble the guide

guide <- add_section(guide, "Dataset Inventory",
                      section_datasets(list(adsl = adsl, adae = adae), metadata = keys),
                      type = "datasets")
guide <- add_section(guide, "Validation Findings", section_findings(findings, explanations),
                      type = "findings")
#> ! 3 finding(s) have no sponsor explanation and are flagged.
guide <- add_section(guide, "Programs Submitted", section_programs(progs), type = "programs")
guide <- add_narrative(
  guide, "Introduction",
  "This SDRG accompanies the CDISCPILOT01 SDTM submission and describes the datasets, conformance findings, and programs delivered."
)
guide
#> 
#> ── CDISCPILOT01 reviewer's guide (SDRG) ────────────────────────────────────────
#>  Sponsor: Acme Pharma
#>  4 section(s)
#>  [datasets] Dataset Inventory  -  2 row(s)
#>  [findings] Validation Findings  -  6 row(s)
#>  [programs] Programs Submitted  -  2 row(s)
#>  [narrative] Introduction  -  128 char(s)

6. Render to a file

out <- tempfile(fileext = ".html")
render_guide(guide, out, format = "html")
#>  Rendered sdrg guide to /tmp/RtmpIp0faG/filebf430149fe575.html
file.exists(out)
#> [1] TRUE

render_guide() starts from the sdrg/adrg section skeleton shipped in inst/templates and needs rmarkdown plus pandoc – no network access or credentials required. rmarkdown is a Suggests, so if it is not installed you get The package "rmarkdown" is required to render a guide file. before any file is written; every other function in the package works without it, since the sections are ordinary tibbles.