Thank you for your interest in contributing to imuGAP! This document explains how to develop, test, and propose changes to the package, as well as the coding conventions, error handling standards, and CI pipelines enforced across the repository.
Code of Conduct
All contributors are expected to be respectful and professional in all interactions — issues, pull requests, code reviews, and discussions. Constructive feedback is welcome; personal attacks and dismissive language are not.
Reporting Bugs and Feature Requests
- Bugs: Use the bug report template. Provide a minimal reproducible example (reprex), session info, and operating system.
-
Features: Use the feature request template. Explain the use case and how it connects to the
imuGAPworkflow (canonicalize-> Stan sampling -> prediction/summary).
Development Workflow & just Recipes
We use just to automate development tasks. All recipes handle namespace and environment configuration automatically:
| Recipe | Description | Equivalent Base Command |
|---|---|---|
just |
Run full validation pipeline: clean, format, lint, docs, test | (compound command) |
just format |
Format R code using air
|
air format . |
just lint |
Lint R code using air and lintr
|
air format . --check && Rscript -e "lintr::lint_package()" |
just docs |
Regenerate roxygen documentation (man/, R/globals.R) |
Rscript -e "roxygen2::roxygenize()" |
just install |
Install package into local R library | R CMD INSTALL . |
just test |
Run complete unit test suite via testthat / devtools
|
Rscript -e "devtools::test()" |
just test-fast |
Run tests, stopping on first failure | Rscript -e "devtools::test(stop_on_failure = TRUE)" |
just coverage |
Measure test coverage via covr
|
Rscript -e "covr::package_coverage()" |
just spell |
Check spelling across docs and vignettes via spelling
|
Rscript -e "spelling::spell_check_package()" |
just render |
Render all vignettes to HTML and PDF | Rscript -e "rmarkdown::render(...)" |
just site / just site-quick
|
Fast build of pkgdown documentation site (no package reinstall) |
Rscript -e "pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)" |
just site-full |
Full build of pkgdown site with package reinstallation (for updated data) |
(compound: install + site) |
just site-preview [item=""] [port=8000] |
Preview pkgdown site on localhost (supports targeted item, e.g. just site-preview imuGAP) |
httpuv::runStaticServer(...) |
just data-inputs |
Regenerate *_sim input datasets from raw simulation |
Rscript data-raw/DATASET.R |
just data-fit |
Regenerate pre-computed Stan fits (fit_sim, target_sim, etc.) |
Rscript data-raw/fit_data.R |
just data |
Regenerate all package data (data-inputs + data-fit) |
(compound command) |
just build |
Build package .tar.gz archive |
R CMD build . |
just check |
Check package archive | R CMD check imuGAP_*.tar.gz --no-manual --no-tests |
just check-cran |
Check package archive using strict CRAN settings | R CMD check imuGAP_*.tar.gz --as-cran |
Code Coverage and Spell Checking
1. Code Coverage (covr)
- Run
just coverageto measure package test coverage. - The CI workflow (
.github/workflows/test-coverage.yaml) runscovr::codecov()on every pull request and uploads reports to Codecov. - Aim to maintain high coverage (>90%, targeting 100%) across all active R source files (
R/canonicalize.R,R/checkers.R,R/helpers.R,R/imuGAP.R,R/methods.R,R/options.R). -
Covered vs. Ignored Files (
.covrignore):-
src/*.{cc,cpp,h}: Generated C++ Stan headers and model exports compiled byrstantoolsfrominst/stan/*.stan. They cannot be instrumented directly bycovr; the underlying models are verified through integration tests (sampling(),predict()). -
R/stanmodels.R: Generated Stan model loader emitted byrstantools::rstan_config(). -
R/flexstanr.R: Generated backend integration shim emitted byflexstanr::use_flexstanr().
-
Code Style, Linting, and Documentation
1. Formatting & Linting
- R code is formatted with
airand linted withlintr(rules in.lintr). - Maximum line length is 100 characters.
-
R/stanmodels.R,R/flexstanr.R,inst/analysis/,inst/scripts/, anddata-raw/are excluded from linting because they are generated artifacts or standalone scratch scripts.
2. Untracked Artifacts & Generated Files
-
Do not hand-edit generated files:
-
R/globals.Randman/*.Rdare produced byroxygen2::roxygenise()(viaroxygen2androxyglobals) and are untracked (#53). Regenerate them withjust docs. - Pre-computed fitted data artifacts (
data/fit_sim*.rda,data/predict_sim*.rda,data/target_sim*.rda) are untracked and generated viajust data-fit. -
R/flexstanr.Ris generated byflexstanr::use_flexstanr().
-
-
Exported Datasets: Document datasets with the
@name <data>/@docType dataidiom inR/imuGAP-package.R.
3. Roxygen Documentation Conventions
-
Explicit
@titleand@description: Always provide explicit@titleand@descriptiontags in roxygen blocks rather than relying on roxygen2’s automatic inference from the initial paragraphs. -
data.tableand@autoglobal: Functions performing calculations or non-standard evaluation withdata.tableshould generally be marked with@autoglobalso thatroxyglobalsautomatically registers referenced columns and symbols inR/globals.R. -
Internal Functions: Unexported helper functions should be tagged with
@keywords internaland@noRdso they are fully documented in source code without generating unneeded.Rdmanual files. -
Markdown Formatting:
roxygen2markdown mode is enabled (Roxygen: list(markdown = TRUE)). Prefer standard markdown syntax:- Use backticks for code identifiers, arguments, and return types (e.g.
`locations`,`data.table`). - Use cross-reference markdown links (e.g.
[sampling()],[flexstanr::stan_options()]). - Use markdown lists, bold text, and tables rather than raw
\code{},\link{}, or\tabular{}Rd tags.
- Use backticks for code identifiers, arguments, and return types (e.g.
4. Roxygen Examples: Dual @examplesIf and \donttest Pattern
For computationally heavy functions (such as sampling() or multi-draw predict()):
-
Always combine
@examplesIf interactive()with\donttest{}:#' @examplesIf interactive() #' \donttest{ #' data("locations_sim") #' data("observations_sim") #' data("populations_sim") #' st_opts <- stan_options(chains = 2, iter = 500) #' sampling( #' observations_sim, populations_sim, locations_sim, #' stan_opts = st_opts #' ) #' } -
Why both are necessary:
-
pkgdownruns\donttest{}blocks during site builds;@examplesIf interactive()evaluates toFALSEduring non-interactive batch builds, keeping site build time fast (~35 seconds instead of >25 minutes). - CRAN checks (
R CMD check --as-cran) look for\donttest{}to skip lengthy runtime checks during package validation. - Interactive user sessions (
example(sampling)) execute normally.
-
5. Modular Stan Architecture
- Stan models in
imuGAPare designed modularly. - Top-level Stan models directly in
inst/stan/(and not Stan code in subdirectories) must remain concise assembly skeletons composed of#include <subpath>.standirectives for particular modular elements (functions/,data/,transformed_data/,parameters/,model/,generated_quantities/). - Never inline full block contents or raw logic directly into top-level models in
inst/stan/; keep component logic encapsulated in dedicated sub-files to facilitate reuse, maintainability, and clean diffs.
6. Vignette Plot Styling, Coordinate Limits & Dark Mode Compatibility
To ensure plots remain clear, readable, and geometrically intact:
- In vignette setup chunks, specify
knitr::opts_chunk$set(dev.args = list(bg = "white")). - Disable automatic plot theme inversion with
if (requireNamespace("thematic", quietly = TRUE)) thematic::thematic_off(). - Configure
ggplot2::theme_set()with solid white backgrounds (plot.background,panel.background,legend.background) and black text (text,axis.text,axis.title,plot.title). -
Coordinate System vs. Scale Limits: Prefer ggplot2 coordinate system bounds (
coord_cartesian(xlim = ..., ylim = ...)) over scale-based limits (scale_*_continuous(limits = ...)) when zooming or adjusting visible ranges. Scale limits discard data points outside the window (altering summary statistics, regressions, or ribbon clipping), whereas coordinate zooming retains all underlying data.
7. Package Reinstallation & Vignette Data
Vignette chunks load data using data(..., package = "imuGAP"), which resolves datasets from the installed package library rather than the working directory. When troubleshooting vignette (and related pkgdown site) issues associated with rendering package example data, if the fix ends up being in the package data (data-raw/DATASET.R or data-raw/fit_data.R), you must reinstall the package (just install or R CMD INSTALL .) before re-rendering vignettes or rebuilding the site with updated data (or use just site-full).
Unit Testing Stan Include Components
Stan code in imuGAP is organized into modular include files in inst/stan/ (across functions/, transformed_data/, model/, etc.). To ensure individual Stan elements function as intended in isolation, we maintain a dedicated Stan unit testing suite in tests/testthat/.
1. Authoring Unit Tests for New Stan Include Files
When adding or refactoring Stan include files, create unit tests following these guidelines:
-
Explicit Target Declaration & Pipelined Harness: Declare
target <- "<subpath>.stan"at the top of the test file, passtargettoskip_if_stan_unchanged(target), and assemble the model harness viasprintf(...) |> compile_stan_harness(). -
Dynamic Expectations from Input Relationships: Express test data dimensions and assertions dynamically using input variables and mathematical relationships (e.g.
length(x),nrow(mat),c(tail(lbounds, -1) - 1L, ubound), analytical closed forms) rather than hardcoding magic numbers repeatedly. -
Test via
rstanDeterministically: Use the internal test helperrun_stan_harness()(defined intests/testthat/helper-stan-test.R), which executesrstan::sampling()usingalgorithm = "Fixed_param",iter = 1,warmup = 0,chains = 1, and a fixed random seed. -
Direct Parameter Extraction & Auto-Reshaping:
run_stan_harness()optionally receives a parameter symbol/name (e.g.run_stan_harness(model, data = ..., out_bounds)) which extracts and reshapes the single-iteration draw to strip the leading singleton iteration dimension (returning a scalar, vector, matrix, or array directly). -
Deterministic Verification:
-
Functions & Transformed Data: Pass fixed deterministic test data in
dataand assign computed values togenerated quantitiesvariables for direct extraction and assertion withexpect_equal(). -
Likelihood & Model Priors: For files evaluating
target += ...or~, userun_stan_harness(..., return_fit = TRUE)and evaluaterstan::log_prob(fit, upars = c(0.0), adjust_transform = FALSE). Note that Stan’s sampling statement~drops normalization constants with respect to parameters, so test against unnormalized log-densities (e.g.sum(dbinom(...) - lchoose(...))). -
1D Array Wrapping: Wrap 1D integer/numeric arrays in
datawithas.array()(e.g.obs_to_weights_bounds = as.array(1L)) so Rstan does not collapse them into scalars.
-
Functions & Transformed Data: Pass fixed deterministic test data in
-
Smart Change-Detection Caching:
- Guard every Stan test block with
skip_if_not_installed("rstan")andskip_if_stan_unchanged(target). - In local development,
skip_if_stan_unchanged()caches MD5 hashes intempdir()to skip model recompilation (~20–25s per model) when the tested Stan files have not changed. - During full checks (
R CMD check,_R_CHECK_PACKAGE_NAME_, orCI), caching is completely bypassed — tests run unconditionally and do not read or write the local cache.
- Guard every Stan test block with
2. Stan Include Coverage Mapping
| Stan Subdirectory | Stan File / Module | Test File | Test Focus & Verification |
|---|---|---|---|
functions/ |
bounds_to_range.stan |
test-stan-bounds_to_range.R |
Index segment calculation and validation for cumulative weight bounds |
layer_offsets.stan |
test-stan-layer_offsets.R |
Multi-layer tree offset accumulation and hierarchical phi calculation | |
lookups.stan |
test-stan-lookups.R |
Column-major index flattening (compute_cdf_lookup, compute_phi_lookup) and bounds validation |
|
unrolled_dose_static_lambda.stan |
test-stan-unrolled_dose.R |
Multi-dose CDF unrolling given schedule and rate | |
convenience.stan |
(composite include) | Tested via constituent sub-function unit tests | |
data/ |
uncensored/, right/, left/
|
(composite includes) | Modular observation data and weights definitions |
locations.stan, structural.stan
|
(composite includes) | Structural indices and location hierarchy data | |
transformed_data/ |
common_indices.stan, layer_phi_lookup.stan
|
test-stan-common_indices.R |
Structural integration for precomputed indices (obs_map_*, cdf_lookup_*, phi_lookup_*) |
layer_indices.stan |
test-stan-layer_indices.R |
Multi-layer location bounds: layer_bounds, parent_child_bounds, loc_layer_idx
|
|
single_phi_lookup.stan |
test-stan-single_phi_lookup.R |
Single-location phi lookups (phi_lookup_*) via subdirectories |
|
model/ |
hierarchical_phi.stan |
test-stan-hierarchical_phi.R |
Deterministic hierarchical observation probabilities against analytical formula |
single_phi.stan |
test-stan-single_phi.R |
Deterministic single-location observation probabilities against analytical formula | |
observation_likelihood.stan |
test-stan-observation_likelihood.R |
Modular observation log-likelihoods (uncensored/, right/, left/) |
Error Messages and Signaling Standards
All user-facing validation errors and warnings should follow these standards:
1. Centralized Format String Constants
-
Define error and warning message format strings as constants at the top of each R file prefixed with
ERR_orMSG_:ERR_MUST_BE_INTEGER <- "`%s` column '%s' must contain integers" ERR_CANNOT_HAVE_NA <- "`%s` column '%s' cannot contain NA values" ERR_OPT_UNKNOWN_MODEL <- "`imugap_opts` unknown model '%s'"
2. Signaling Functions: stop_fmt_if and warn_fmt_if
-
Use internal helpers
stop_fmt_if()andwarn_fmt_if()for validation assertions:stop_fmt_if( !all(as.integer(dt[, get(x)]) == dt[, get(x)]), ERR_MUST_BE_INTEGER, deparse(substitute(dt)), x, n = n + 1L ) Use the parameter
nto adjust the call stack offset so the error is attributed to the user’s top-level function call rather than internal helper functions.
3. Typography: Backticks vs. Single Quotes
Follow a strict convention when formatting error and warning strings:
-
Backticks (
`code`): Use for formal R code symbols, argument names, function names, expressions, and classes:`observations` must be a data.frame`df` must be a single positive integer`stan_opts` must be created by stan_options()
-
Single Quotes (
'value'): Use for user-supplied string values, column names, model names, or discrete inputs:column '%s' cannot contain NA valuesunknown model '%s''%s' must be numeric
Stan Backend and Dependencies
-
flexstanr: Portable Stan backend support is provided by the imported packageflexstanr (>= 0.2.0). The integration helperR/flexstanr.Ris generated byflexstanr::use_flexstanr(). -
cmdstanr: An optional, non-CRANSuggests. It is resolved in CI viaRemotes: stan-dev/cmdstanrinDESCRIPTION. -
Stan Stack Pinning: Do not add the
stan-devr-universe as an extra repository in CI workflows:pakwould then select dev builds ofStanHeaders/rstan, which fail to compile against CRAN’sRcppEigen(#101).Remotespins onlycmdstanrwhile keeping the remainder of the Stan stack on CRAN.
Pull Request and CI Workflows
Every pull request triggers four automated GitHub Actions workflows:
-
R-CMD-check: RunsR CMD check --as-cranacross Ubuntu, macOS, and Windows on R release, oldrel, and devel (9 jobs). -
lint: Verifies formatting withair format . --checkand lint rules withlintr::lint_package(). -
test-coverage: Computes code coverage withcovrand uploads results to Codecov. -
pkgdown: Builds the documentation site and confirms that all vignettes compile cleanly. Deployed to GitHub Pages upon push tomainand published releases.
All checks must pass before merging.
