| name | acid-r-package |
| description | Acid Genomics R package development conventions — per-project air.toml, global ~/.lintr only, roxygen2 8.x @importFrom split, S4 linter tuning, AcidDevTools::check() gate. Use when writing or checking an acidgenomics R package, configuring air/lintr, or fixing roxygen/lint failures. |
Acid Genomics R Package Development Conventions
Applies to all packages under ~/git/personal/r-<pkgname> (acidgenomics org).
Reference implementation: pointillism 0.8.0 (2026-06-20).
Tooling Stack
| Tool | Config | Notes |
|---|
| formatter | air.toml (per-project) | No global config — air walks up from file; must exist at package root |
| linter | ~/.lintr (global, chezmoi-managed) | No per-project .lintr — project file replaces global, never merges |
| docs | roxygen2 8.0.0 | Single-line @importFrom required (see below) |
| check | AcidDevTools::check() | Wraps lint + rcmdcheck + BiocCheck |
air.toml
All packages carry this identical 8-line file. The canonical source is
~/git/personal/r-pipette/air.toml. Copy verbatim — do not vary any field:
[format]
default-exclude = false
exclude = []
indent-style = "space"
indent-width = 4
line-ending = "lf"
line-width = 80
persistent-line-breaks = true
Air has no global config fallback — per-project air.toml is required and
must be git-tracked (not gitignored). Untracked air.toml is invisible to
fresh clones and koopa app r publish.
~/.lintr (chezmoi source: opt/dotfiles/chezmoi/dot_lintr)
Single global file; no per-project .lintr files — any local .lintr in a
package directory silently replaces the global rather than merging. Remove
with git rm -f .lintr on all packages.
To change ~/.lintr: edit opt/dotfiles/chezmoi/dot_lintr, then:
chezmoi apply --source=~/.local/share/koopa/opt/dotfiles/chezmoi ~/.lintr
Current critical settings (2026-06-19)
object_usage_linter = NULL,
object_name_linter(
styles = c("camelCase", "CamelCase", "dotted.case"),
regexes = c("^as\\.")
),
cyclocomp_linter(complexity_limit = 45L),
DCF format rule
exclusions: key must appear on the line immediately after the closing )
of the linters_with_defaults(...) block — no blank line between them. A blank
line starts a new DCF record and the exclusions key is silently dropped.
lintr parser crashes on Unicode-escaped named vector keys
lintr 3.3 crashes (subscript out of bounds) when parsing files containing
"\uXXXX" = value syntax in named vector literals. This is a lintr bug.
Fix: restructure using setNames() with separate object and nm args,
using actual UTF-8 characters in nm — or namespace with base::setNames().
myMap <- c("Α" = "Alpha", "Β" = "Beta")
myMap <- setNames(
object = c("Alpha", "Beta"),
nm = c("Α", "Β")
)
Note: setNames is in base R and requires no @importFrom. Do NOT write
@importFrom base setNames — base does not export setNames in the
@importFrom sense and roxygen2 will warn.
.gitignore Convention
Per-package .gitignore should be minimal. The global
~/.config/git/ignore (chezmoi-managed) covers: .RData, .Rcheck,
.Rhistory, .Rproj.user, doc/, docs/, tests/testthat/_problems/.
Standard per-package .gitignore:
# This file intentionally minimal — see ~/.config/git/ignore for global patterns.
Exception — packages with committed test data (e.g. r-acidtest):
# This file intentionally minimal — see ~/.config/git/ignore for global patterns.
!data/
!data/*.rda
!data-raw/
Do NOT include .RData, .Rcheck, .Rhistory, .Rproj.user, or docs/ in
per-package .gitignore — they are in the global.
DESCRIPTION Conventions
Config/roxygen2/version: 8.0.0 # NOT RoxygenNote: 7.3.x
Roxygen: list(markdown = TRUE)
Config/testthat/edition: 3
Config/testthat/parallel: true
License: Apache License (>= 2) # NOT AGPL-3; NOT "| file LICENSE"
Run devtools::document() last (after all hand-edits) so it regenerates man/
and NAMESPACE over a clean state.
License Files
Apache-licensed packages use LICENSE.md (markdown, from
usethis::use_apache_license()) — not a plain LICENSE file.
In .Rbuildignore: ^LICENSE\.md$ (not ^LICENSE$).
The koopa-r-release skill covers the full relicensing procedure.
README Canonical Format
Header: line 3 must have both badges in this order — bioconda first, lifecycle
second. Use https:// in all badge links; no ?style=flat:
[](https://bioconda.github.io/recipes/r-<pkg>/README.html) 
Conda install block (after R install block):
### [Conda][] method
Configure [Conda][] to use the [Bioconda][] channels.
```sh
# Don't install recipe into base environment.
name='r-<pkg>'
conda create --name="$name" "$name"
conda activate "$name"
R
Link refs (before License section):
```markdown
[bioconda]: https://bioconda.github.io/
[conda]: https://docs.conda.io/
License section (always last):
## License
Apache-2.0 — Copyright <YEAR> Acid Genomics LLC — see [LICENSE.md](LICENSE.md).
Copyright year = year of first git commit:
git log --all --reverse --format="%ad" --date=format:"%Y" | head -1
roxygen2 8.x — @importFrom Format
Breaking change from 7.x: each @importFrom tag must be a single source
line. Multi-line continuation (#' wrapping) is an error.
But line_length_linter fires at 80 chars. Solution: split into multiple
separate @importFrom tags for the same package — one logical group per line:
Never use # nolint on #' comment lines — roxygen2 8.x parses # and
nolint as additional function names to import.
setGeneric(name = "PascalCase") — object_name_linter False Positive
object_name_linter fires on the string value passed to
setGeneric(name = "SeuratMarkers") because it looks like a PascalCase
variable. These are S4 generic names and cannot be renamed. Suppress with
# nolint on the name = line only:
setGeneric(
name = "SeuratMarkers",
...
)
S4 Method Argument Order — function_argument_linter
function_argument_linter requires arguments without defaults to come before
arguments with defaults. For S4 method dispatchers that use formals() to set
defaults after the function definition, give the argument an explicit sentinel
default in the signature to satisfy lintr:
`correlation,matrix,missing` <- function(x, y = NULL, method) { ... }
formals(`correlation,matrix,missing`)[["method"]] <- .method
`correlation,matrix,missing` <- function(x, y = NULL, method = .method) { ... }
formals(`correlation,matrix,missing`)[["method"]] <- .method
AcidGenomes API Changes (≥ 0.8.x)
makeTxToGeneFromEnsembl() was removed. Build a TxToGene from Ensembl with:
gr <- AcidGenomes::makeGRangesFromEnsembl(
organism = "Homo sapiens",
level = "transcripts",
genomeBuild = "GRCh38",
release = 87L,
ignoreVersion = TRUE
)
tx2gene <- AcidGenomes::TxToGene(gr)
AcidDevTools::check() — Full Pre-Release Gate
AcidDevTools::check(
path = "~/git/personal/r-<pkg>",
lints = TRUE,
urls = FALSE,
cran = FALSE,
biocCheck = TRUE
)
Gate order: dependency check → lintr → urlchecker (urls=TRUE only) →
rcmdcheck → BiocCheck (when biocViews present).
Always run from a temp directory to prevent *.Rcheck/ from being created
in your current working directory:
old <- setwd(tempdir())
on.exit(setwd(old))
AcidDevTools::check(path = path.expand("~/git/personal/r-<pkg>"), ...)
check() fails if any Suggests package is missing, even before running
lints. Install all Suggests before running the gate.
Parallel test failures under rcmdcheck: Config/testthat/parallel: true
can crash in rcmdcheck's subprocess on macOS (fork resource limits). Run
TESTTHAT_PARALLEL=false as an env override — keep the DESCRIPTION setting as-is:
Sys.setenv(TESTTHAT_PARALLEL = "false")
AcidDevTools::check(...)
valid() test requires internet: AcidDevTools::valid() calls
utils::old.packages() which needs network. Guard with skip_if_not(goalie::hasInternet()).
keyword_quote_linter — Named List Elements
This linter was removed from ~/.lintr due to pervasive S4 false
positives (S4 class definitions require quoted strings in contains, slots,
prototype — lintr cannot distinguish these from cosmetic quoting). The note
below is for reference only, in case it is re-enabled:
Quoted names in list(...) fire when the name is a valid R symbol:
list("geneId" = val)
list(geneId = val)
Exception: switch() EXPR values are strings and must stay quoted.
leftJoin — Type Coercion Before Join
AcidPlyr::leftJoin() enforces strict type matching on by columns. Coerce
geneId to character on both sides:
markers[["geneId"]] <- as.character(mcols(ranges)[["geneId"]])
known[["geneId"]] <- as.character(known[["geneId"]])
x <- leftJoin(x, known, by = "geneId")
future::plan() — Defunct Backends
"multiprocess" was removed in future 1.32. Use "multicore":
if (isTRUE(future::supportsMulticore()) && !is.null(workers)) {
future::plan("multicore", workers = workers)
}
Running Tests
Always set locale to avoid S4 method dispatch encoding warnings:
LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 \
TESTTHAT_PARALLEL=false \
R -q -e 'devtools::load_all("."); testthat::test_dir("tests/testthat")'
Config/testthat/parallel: true stays in DESCRIPTION — TESTTHAT_PARALLEL=false
is a runner-only override for debuggable serial output.
@importFrom base — Do Not Use
base package functions (setNames, inherits, which, etc.) are always
available in R without explicit import. Never write @importFrom base <fn>:
- roxygen2 warns: "Excluding unknown export from base:
setNames"
- The function is unavailable to
@importFrom regardless
Use the function unqualified. If namespace_linter flags it incorrectly, that
is a lintr false positive — add # nolint to that specific line.
Version Bump Rule
If a package's Version: in DESCRIPTION matches what's already in
src/contrib/PACKAGES on S3, bump the patch before publishing — S3 overwrites
but the PACKAGES index must show a new version to be useful.
README, .gitignore, and air.toml changes do not require a version bump
or koopa app r publish — documentation-only PRs can be merged without
republishing to S3.
See koopa-r-release for the complete publish workflow.
AcidDevTools::valid() — Bioconductor Source/Binary Lag
valid() calls BiocManager::valid() which compares installed versions against
Bioconductor repo versions. When Bioconductor releases a source update but the
binary hasn't been built yet for sonoma-arm64, it falsely flags the package as
outdated — installing would force source compilation with no benefit.
Fixed in AcidDevTools ≥ 0.7.11: valid() now filters BiocManager::valid()
results to only flag packages where a binary is available at the newer version.
Packages in source-only lag state (e.g. AnnotationHub 4.2.0 installed, 4.2.1
source-only) are silently skipped until the binary lands.
.requireNamespaces() with Base Packages
base R packages (utils, methods, parallel, tools, stats) must NOT
be passed to .requireNamespaces(). In R CMD check subprocesses, these
packages may fail requireNamespace() even though they're always available,
causing tests to fail spuriously.
stopifnot(.requireNamespaces("utils"))
x <- utils::object.size(x)
x <- utils::object.size(x)
R CMD check examples failures from Suggests
R CMD check runs @examples in a subprocess without Suggests available.
Any example that calls a function requiring a Suggests package will fail with
Error: .requireNamespaces("pkg") is not TRUE.
Fix: use the ## > prefix on @examples lines to show in docs but skip
execution:
NEVER use # nolint on #' lines — roxygen2 8.x parses # and nolint as
function names to import.
S3 Binary Archive Cleanup
koopa app r archive only cleans src/contrib/. Old .tgz files in
bin/macosx/sonoma-arm64/contrib/4.6/ must be deleted manually after a sweep:
aws s3 rm s3://<bucket>/bin/macosx/sonoma-arm64/contrib/4.6/<Pkg>_<old>.tgz \
--profile=acidgenomics
koopa app r reindex
Orphan binaries (binaries with no matching source package) must also be
deleted. Use koopa app r clean-orphan-binaries (added 2026-06-20) to detect
and remove them automatically.
CloudFront Cache Stale After Manual S3 Operations
After any manual aws s3 operations on the binary prefix, always follow with
koopa app r reindex to regenerate manifests and invalidate CloudFront.
Without this, R clients may see stale PACKAGES manifests for up to 60 seconds
after the S3 change.
koopa app r clean-orphan-binaries
New command (2026-06-20): detects and deletes binary .tgz files in the active
binary prefix that have no corresponding source tarball in src/contrib/. These
orphan binaries prevent clean install behavior (R picks the binary but no source
matches, causing version mismatches).
koopa app r clean-orphan-binaries
koopa app r clean-orphan-binaries --no-invalidate