Skip to contents

Thanks for considering a contribution. whep processes agro-environmental data for the Who Has Eaten the Planet project, funded by the European Research Council.

This is a research codebase, so parts of it are unavoidably technical: nitrogen and carbon balances, gridded land-use modelling, multi-regional input–output accounting. You do not need to understand any of that to make a useful contribution. A good share of the open work is ordinary R engineering — dead code, argument validation, missing tests, documentation — and we label it as such so you can find it.

Finding something to work on

Start with good first issue. Those issues carry a comment aimed specifically at a first-time contributor: whether the task needs data or network access, the exact file:line to look at, what “done” looks like, and which test file to touch.

help wanted is the wider pool of issues nobody is actively working on.

no-data-needed is the one to reach for if you cannot get the project’s input data — see the next section. It means the issue can be reproduced, fixed and verified with nothing but a clone of the repository.

Two more labels tell you what kind of review to expect, and they are worth understanding before you pick something:

Label What it means
mechanical No methodological decision. The change is objectively correct, and green CI plus a test is close to sufficient to merge.
needs-expert Embeds a scientific or methodological decision. Will not be merged on green CI alone; a domain expert has to weigh in.

If you are new here, prefer mechanical. A needs-expert issue may be small in diff terms and still need weeks of discussion about what the right number is.

Comment on an issue before starting substantial work, so two people don’t write the same patch. For a one-line fix, just open the PR.

Do I need the data?

Usually not — and this is the question that trips people up, so it is worth being explicit.

The test suite is entirely offline. No credentials, no downloads, no pinned datasets. A dedicated offline-tests CI job enforces this by running the suite with an empty cache behind a dead proxy, so any test that reaches the network fails deterministically. You can therefore clone, install dependencies, and run devtools::test() with no access to anything.

The full data pipeline is a different matter. Running a real build needs inputs that are not in the repository:

  • Pinned inputs are fetched over the network from the project’s pins board, registered in inst/extdata/whep_inputs.csv.
  • Multi-GB rasters (CRU climate, LPJmL output, HYDE, LUH2, HWSD, and others) live on local disk and are located through environment variables — WHEP_CRU_DIR, WHEP_LUH2_DIR, WHEP_HWSD_DIR, and friends. The readers abort with an instruction when a variable is unset; never hardcode a path.

So: if an issue is about a pipeline result being wrong, you probably need the data and should say so on the issue. If it is about dead code, a crash, argument validation, documentation, or a missing test, you almost certainly do not.

You do not have to work this out yourself. Issues carrying no-data-needed have been checked: the fix and its verification need only the repository — package data under data/ and inst/extdata/, hand-built tibble::tribble() fixtures, and arguments you inject yourself all count as available. If you find an issue with that label that turns out to need data after all, please say so on the issue; the label is only worth having if it is reliable.

Setting up

You need R >= 4.1.0 (the codebase uses the native |> pipe throughout).

install.packages("devtools")
devtools::install_deps(dependencies = TRUE)
devtools::load_all()

You also need the air formatter binary on your PATH — see the formatting section below. It is not an R package; download a release for your platform.

Formatting, which CI does not check before merge

air format .

This is mandatory and it is not optional or approximate, even though no check will fail if you skip it. Nothing gates formatting on a pull request; the format-main workflow reformats main after every merge instead. That is there for the times someone forgets, and leaning on it has a real cost: the diff a reviewer read stops being the diff that landed, and main collects formatting-only commits. So run the binary yourself — hand-matching the style does not work. Note that it formats every .R file in the repo — R/, tests/, data-raw/ — not only the ones you edited, so commit only your own hunks if it reformats something unrelated.

The five CI checks, and how to run them locally

Five workflows run on every pull request, including one from a fork. Four of them gate the merge; test-coverage is informational. Each is reproducible on your machine, and doing that first is much faster than pushing and waiting.

1. lintlintr

lintr::lint_package(
  linters = lintr::linters_with_defaults(
    object_usage_linter = NULL,
    line_length_linter = NULL,
    indentation_linter = NULL,
    commas_linter = NULL
  )
)

Those four linters are disabled because they conflict with air’s output. inst/scripts and inst/analysis are excluded via .lintr.

Note that line_length_linter being off does not license long lines — the 80-character maximum is still a house rule (see below), it just is not machine enforced.

2. R-CMD-check — five platforms

macOS release, Windows release, and Ubuntu on R-devel, release and oldrel-1. No errors, no warnings, no notes. This job also runs the test suite, so a failing test turns up here as a hard ERROR.

To check without waiting for the tests:

rcmdcheck::rcmdcheck(
  build_args = "--no-build-vignettes",
  args = c("--no-tests", "--ignore-vignettes"),
  error_on = "error"
)

Two things account for most avoidable failures here:

3. The test suite

Run it directly while you work, rather than through the full check:

devtools::test()                          # everything
devtools::test(filter = "footprint")      # one file

The whole suite must be green — 100%, no skips added to get there.

4. offline-tests — no test may touch the network

Reproduce the CI condition exactly:

XDG_CACHE_HOME=$(mktemp -d) http_proxy=http://127.0.0.1:9 \
  https_proxy=http://127.0.0.1:9 Rscript -e 'devtools::test()'

If this job fails on its own while the ordinary test job passes, you have added a test that quietly fetches something. The fix is to give it an offline fixture, not to skip the test and not to relax the job.

5. pkgdown — the documentation site

Every documented topic — functions and datasets with roxygen docs — must appear in _pkgdown.yml under reference:. Adding an exported function without listing it there fails the build. Check with:

comm -23 \
  <(ls man/*.Rd | sed 's|man/||;s|\.Rd||' | grep -v whep-package | sort) \
  <(grep '^  - ' _pkgdown.yml | sed 's/^  - //' | sort)

Empty output means you are fine.

And one that does not gate

test-coverage runs the suite under covr and reports to Codecov. Treat a coverage drop as a prompt to add a test, not as a blocker.

All five gating checks run on pull requests from forks, which is the normal path for an outside contribution, so you get the same signal we do.

The short version, before you push

air format .
devtools::document()
rcmdcheck::rcmdcheck(
  build_args = "--no-build-vignettes",
  args = c("--no-tests", "--ignore-vignettes"),
  error_on = "error"
)
devtools::test()

Code style

The baseline is the tidyverse style guide, with air as the arbiter of formatting. On top of that, this repo has conventions worth knowing before you write anything — they come up in review constantly:

Layout

  • Maximum line width is 80 characters.
  • Exported functions come first in a file; private helpers, prefixed with ., come at the end.
  • Functions should be short — 25 lines is the target. Split large ones into named helpers rather than adding comments to a long body.
  • No functions defined inside functions. Everything at top level.
  • No more than 5 arguments in a signature; group related ones into a named list.
  • Function and variable names stay under 30 characters.

R idiom

  • Always use the native pipe |>. Write functions so they read as piped expressions.
  • Always namespace imported functions: dplyr::filter(), never bare filter(). Do not use @importFrom.
  • Avoid for loops — reach for vectorised operations, purrr, or dplyr/tidyr. (Exception: a data.table helper iterating a small fixed set of column names.)
  • Use stringr rather than base R for string work.
  • Use the .by argument for grouping.
  • Escaped regex characters need double-escaping in R strings: "\\.", not "\.".
  • When a function takes column names as arguments, expect them unquoted and use {{ }} inside.

Data structures

  • This is a tidy-data project. Exported functions accept and return tibbles. Private helpers may use data.table internally for speed but must convert back before returning. Never a bare data.frame.
  • Use tibble::tribble() for small inline tables.
  • Column names are snake_case and must be self-explanatory. Write ne_maintenance, volatile_solids, methane_potential — not NEm, VS, Bo.
  • Do not carry redundant name/code column pairs (area alongside area_code) through intermediate steps. Integer codes are enough as join keys internally; human-readable names get joined on at the final output stage.

Errors and validation

Multi-method functions

An estimation function with more than one defensible method exposes a method = or tier = argument, defaulting to the most rigorous available. Simpler methods stay selectable for sensitivity analysis. They are alternatives, never silent fallbacks: the chosen method is recorded in an output column, and a coarser one is used only when explicitly asked for.

Documentation

Use roxygen2, and document exported functions only — private . helpers need no roxygen block.

  • First line is the title, with no @title tag: short, verb in the imperative. Then @description, @param for each argument, @return, @export, @examples.
  • One space after #'; indent continuation lines by two.
  • End every documentation sentence with a full stop.

Examples must run. Never use \dontrun{} or \donttest{} — everything runs during R CMD check. For a function that depends on remote data or is slow, use the example = FALSE pattern: add an example argument that returns a small hardcoded tibble::tribble() fixture from R/toy_examples.R, and let @examples call my_function(example = TRUE). See build_primary_production() for the reference implementation. For anything that runs fast on small input, just write a self-contained inline example.

Run devtools::document() after any roxygen change, and commit the regenerated man/ files.

Tests

Tests use testthat (edition 3), one test file per R script: R/thing.R is tested by tests/testthat/test_thing.R.

  • Reach exported objects through whep::name, not bare names. For dynamic access in a loop, getExportedValue("whep", nm). Never ::: or getFromNamespace() for something exported.
  • Build fixtures with tibble::tribble(); assert with testthat expectations or pointblank.
  • Factor repeated fixture setup into helpers.
  • tests/testthat/test_gapfilling.R is a good model to imitate.
  • Every test must pass offline. See check 5 above.

Changing packaged data

The .rda files under data/ are build products. When you edit a CSV in inst/extdata/harmonization/, regenerate them:

  1. Edit the CSV.
  2. Rscript data-raw/harmonization_tables.R — rebuilds the .rda files.
  3. Rscript data-raw/table_mappings.R — only if regions.csv or items_*.csv changed.
  4. Rscript data-raw/whep_inputs.R — only if whep_inputs.csv changed.

Commit both the CSV and the regenerated .rda.

Citations and references

This one matters more here than in most repositories, and there is no flexibility in it:

Never guess or invent a reference title, author, year, or DOI. Verify every bibliographic detail against the actual source — the paper, the PDF, a DOI lookup — before you write it down.

If you cannot verify a citation, say so explicitly in the PR rather than supplying a plausible-looking one. A wrong DOI in a science package is worse than an absent one, because it looks checked.

Opening a pull request

The project follows this workflow; it is worth a read.

  • Branch from main. Name branches yourname/short-topic or type/short-topic — e.g. docs/contributing-guidelines, chore/remove-renv.
  • Keep a PR to one concern. A focused diff gets reviewed; a sprawling one waits.
  • Reference the issue it addresses.
  • Describe what you measured, not only what you changed. This codebase’s issues are written with numbers in them — a before/after count, a total that now balances, the test that fails without your fix — and PRs are read the same way. “Fixes the drop” is much weaker than “recovers 1.66 Tg that the filter discarded; total now closes against the polity table to 1e-10”.
  • If you touched a needs-expert area, say plainly which choice you made and why, so the expert reviewing it knows where to look.
  • Run all six checks locally first.

Do not be discouraged if a small PR draws methodological discussion. It usually means you found something real that nobody had decided yet.

Reporting a bug

Please include:

  • What you ran, as a copy-pasteable snippet, and what happened.
  • The version of whep (or the commit), your R version, and your platform.
  • Whether it reproduces on a clean main.
  • For anything numeric: the measured magnitude. “Global total is 3.7% low” is actionable; “the numbers look off” cannot be triaged.

If a value is wrong rather than the code being broken, say which quantity, for which year and area, and against what reference you compared it.

Conduct

Be decent to each other. Assume the person on the other side of the review is acting in good faith and knows something you don’t. Critique the code and the method, not the person.

This project is released with a Contributor Code of Conduct, which is the rOpenSci Code of Conduct (also available in Spanish). By contributing, you agree to abide by its terms. It sets out what counts as unacceptable behaviour and how to report it; the maintainers may also remove comments, commits or contributors as needed.